mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 09:43:23 +00:00
Merged branch with master and fixed conflicts
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
firstValueFrom,
|
||||
switchMap,
|
||||
Subject,
|
||||
catchError,
|
||||
from,
|
||||
of,
|
||||
finalize,
|
||||
takeUntil,
|
||||
defer,
|
||||
throwError,
|
||||
} from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
|
||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
enum State {
|
||||
NewUser,
|
||||
ExistingUserUntrustedDevice,
|
||||
}
|
||||
|
||||
type NewUserData = {
|
||||
readonly state: State.NewUser;
|
||||
readonly organizationId: string;
|
||||
readonly userEmail: string;
|
||||
};
|
||||
|
||||
type ExistingUserUntrustedDeviceData = {
|
||||
readonly state: State.ExistingUserUntrustedDevice;
|
||||
readonly showApproveFromOtherDeviceBtn: boolean;
|
||||
readonly showReqAdminApprovalBtn: boolean;
|
||||
readonly showApproveWithMasterPasswordBtn: boolean;
|
||||
readonly userEmail: string;
|
||||
};
|
||||
|
||||
type Data = NewUserData | ExistingUserUntrustedDeviceData;
|
||||
|
||||
@Directive()
|
||||
export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected State = State;
|
||||
|
||||
protected data?: Data;
|
||||
protected loading = true;
|
||||
|
||||
// Remember device means for the user to trust the device
|
||||
rememberDeviceForm = this.formBuilder.group({
|
||||
rememberDevice: [true],
|
||||
});
|
||||
|
||||
get rememberDevice(): FormControl<boolean> {
|
||||
return this.rememberDeviceForm?.controls.rememberDevice;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected formBuilder: FormBuilder,
|
||||
protected devicesService: DevicesServiceAbstraction,
|
||||
protected stateService: StateService,
|
||||
protected router: Router,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected messagingService: MessagingService,
|
||||
protected tokenService: TokenService,
|
||||
protected loginService: LoginService,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected cryptoService: CryptoService,
|
||||
protected organizationUserService: OrganizationUserService,
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected validationService: ValidationService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = true;
|
||||
|
||||
this.setupRememberDeviceValueChanges();
|
||||
|
||||
// Persist user choice from state if it exists
|
||||
await this.setRememberDeviceDefaultValue();
|
||||
|
||||
try {
|
||||
const accountDecryptionOptions: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
|
||||
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
|
||||
// can we check if they have a user key or master key in crypto service? Would that be sufficient?
|
||||
if (
|
||||
!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
|
||||
!accountDecryptionOptions?.hasMasterPassword
|
||||
) {
|
||||
// We are dealing with a new account if:
|
||||
// - User does not have admin approval (i.e. has not enrolled into admin reset)
|
||||
// - AND does not have a master password
|
||||
|
||||
this.loadNewUserData();
|
||||
} else {
|
||||
this.loadUntrustedDeviceData(accountDecryptionOptions);
|
||||
}
|
||||
|
||||
// Note: this is probably not a comprehensive write up of all scenarios:
|
||||
|
||||
// If the TDE feature flag is enabled and TDE is configured for the org that the user is a member of,
|
||||
// then new and existing users can be redirected here after completing the SSO flow (and 2FA if enabled).
|
||||
|
||||
// First we must determine user type (new or existing):
|
||||
|
||||
// New User
|
||||
// - present user with option to remember the device or not (trust the device)
|
||||
// - present a continue button to proceed to the vault
|
||||
// - loadNewUserData() --> will need to load enrollment status and user email address.
|
||||
|
||||
// Existing User
|
||||
// - Determine if user is an admin with access to account recovery in admin console
|
||||
// - Determine if user has a MP or not, if not, they must be redirected to set one (see PM-1035)
|
||||
// - Determine if device is trusted or not via device crypto service (method not yet written)
|
||||
// - If not trusted, present user with login decryption options (approve from other device, approve with master password, request admin approval)
|
||||
// - loadUntrustedDeviceData()
|
||||
} catch (err) {
|
||||
this.validationService.showError(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async setRememberDeviceDefaultValue() {
|
||||
const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice();
|
||||
|
||||
const rememberDevice = rememberDeviceFromState ?? true;
|
||||
|
||||
this.rememberDevice.setValue(rememberDevice);
|
||||
}
|
||||
|
||||
private setupRememberDeviceValueChanges() {
|
||||
this.rememberDevice.valueChanges
|
||||
.pipe(
|
||||
switchMap((value) =>
|
||||
defer(() => this.deviceTrustCryptoService.setShouldTrustDevice(value))
|
||||
),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async loadNewUserData() {
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.stateService.getUserSsoOrganizationIdentifier()
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
return throwError(() => new Error(this.i18nService.t("ssoIdentifierRequired")));
|
||||
}
|
||||
|
||||
return from(this.organizationApiService.getAutoEnrollStatus(organizationIdentifier));
|
||||
}),
|
||||
catchError((err: unknown) => {
|
||||
this.validationService.showError(err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
|
||||
const email$ = from(this.stateService.getEmail()).pipe(
|
||||
catchError((err: unknown) => {
|
||||
this.validationService.showError(err);
|
||||
return of(undefined);
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
);
|
||||
|
||||
const autoEnrollStatus = await firstValueFrom(autoEnrollStatus$);
|
||||
const email = await firstValueFrom(email$);
|
||||
|
||||
this.data = { state: State.NewUser, organizationId: autoEnrollStatus.id, userEmail: email };
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) {
|
||||
this.loading = true;
|
||||
|
||||
const email$ = from(this.stateService.getEmail()).pipe(
|
||||
catchError((err: unknown) => {
|
||||
this.validationService.showError(err);
|
||||
return of(undefined);
|
||||
}),
|
||||
takeUntil(this.destroy$)
|
||||
);
|
||||
|
||||
email$
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
})
|
||||
)
|
||||
.subscribe((email) => {
|
||||
const showApproveFromOtherDeviceBtn =
|
||||
accountDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
|
||||
|
||||
const showReqAdminApprovalBtn =
|
||||
!!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
|
||||
|
||||
const showApproveWithMasterPasswordBtn =
|
||||
accountDecryptionOptions?.hasMasterPassword || false;
|
||||
|
||||
const userEmail = email;
|
||||
|
||||
this.data = {
|
||||
state: State.ExistingUserUntrustedDevice,
|
||||
showApproveFromOtherDeviceBtn,
|
||||
showReqAdminApprovalBtn,
|
||||
showApproveWithMasterPasswordBtn,
|
||||
userEmail,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async approveFromOtherDevice() {
|
||||
if (this.data.state !== State.ExistingUserUntrustedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginService.setEmail(this.data.userEmail);
|
||||
this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
async requestAdminApproval() {
|
||||
this.loginService.setEmail(this.data.userEmail);
|
||||
this.router.navigate(["/admin-approval-requested"]);
|
||||
}
|
||||
|
||||
async approveWithMasterPassword() {
|
||||
this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } });
|
||||
}
|
||||
|
||||
async createUser() {
|
||||
if (this.data.state !== State.NewUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.loading to support clients without async-actions-support
|
||||
this.loading = true;
|
||||
try {
|
||||
const { publicKey, privateKey } = await this.cryptoService.initAccount();
|
||||
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||
await this.apiService.postAccountKeys(keysRequest);
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("accountSuccessfullyCreated")
|
||||
);
|
||||
|
||||
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
|
||||
|
||||
if (this.rememberDeviceForm.value.rememberDevice) {
|
||||
await this.deviceTrustCryptoService.trustDevice();
|
||||
}
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
logOut() {
|
||||
this.loading = true; // to avoid an awkward delay in browser extension
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
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 { MasterKey, UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { PasswordColorText } from "../../shared/components/password-strength/password-strength.component";
|
||||
|
||||
@Directive()
|
||||
@@ -44,7 +44,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyService: PolicyService,
|
||||
protected stateService: StateService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -79,23 +79,28 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
if (this.kdfConfig == null) {
|
||||
this.kdfConfig = await this.stateService.getKdfConfig();
|
||||
}
|
||||
const key = await this.cryptoService.makeKey(
|
||||
|
||||
// Create new master key
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
email.trim().toLowerCase(),
|
||||
this.kdf,
|
||||
this.kdfConfig
|
||||
);
|
||||
const masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
|
||||
const newMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey
|
||||
);
|
||||
|
||||
let encKey: [SymmetricCryptoKey, EncString] = null;
|
||||
const existingEncKey = await this.cryptoService.getEncKey();
|
||||
if (existingEncKey == null) {
|
||||
encKey = await this.cryptoService.makeEncKey(key);
|
||||
let newProtectedUserKey: [UserKey, EncString] = null;
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
if (userKey == null) {
|
||||
newProtectedUserKey = await this.cryptoService.makeUserKey(newMasterKey);
|
||||
} else {
|
||||
encKey = await this.cryptoService.remakeEncKey(key);
|
||||
newProtectedUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
}
|
||||
|
||||
await this.performSubmitActions(masterPasswordHash, key, encKey);
|
||||
await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
@@ -105,9 +110,9 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: [SymmetricCryptoKey, EncString]
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString]
|
||||
) {
|
||||
// Override in sub-class
|
||||
}
|
||||
@@ -162,7 +167,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -173,7 +178,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -184,7 +189,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "exposedMasterPassword" },
|
||||
content: { key: "exposedMasterPasswordDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -201,7 +206,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
|
||||
@@ -4,16 +4,18 @@ import { firstValueFrom, Subject } from "rxjs";
|
||||
import { concatMap, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.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 { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
|
||||
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/enums";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -23,10 +25,10 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
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 { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class LockComponent implements OnInit, OnDestroy {
|
||||
@@ -34,20 +36,20 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
pin = "";
|
||||
showPassword = false;
|
||||
email: string;
|
||||
pinLock = false;
|
||||
pinEnabled = false;
|
||||
masterPasswordEnabled = false;
|
||||
webVaultHostname = "";
|
||||
formPromise: Promise<MasterPasswordPolicyResponse>;
|
||||
supportsBiometric: boolean;
|
||||
biometricLock: boolean;
|
||||
biometricText: string;
|
||||
hideInput: boolean;
|
||||
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected onSuccessfulSubmit: () => Promise<void>;
|
||||
|
||||
private invalidPinAttempts = 0;
|
||||
private pinSet: [boolean, boolean];
|
||||
private pinStatus: PinLockType;
|
||||
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
|
||||
@@ -65,12 +67,13 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
protected stateService: StateService,
|
||||
protected apiService: ApiService,
|
||||
protected logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
protected ngZone: NgZone,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: InternalPolicyService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
protected userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -90,7 +93,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.pinLock) {
|
||||
if (this.pinEnabled) {
|
||||
return await this.handlePinRequiredUnlock();
|
||||
}
|
||||
|
||||
@@ -102,7 +105,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
@@ -115,18 +118,18 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const success = (await this.cryptoService.getKey(KeySuffixOptions.Biometric)) != null;
|
||||
const userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
|
||||
|
||||
if (success) {
|
||||
await this.doContinue(false);
|
||||
if (userKey) {
|
||||
await this.setUserKeyAndContinue(userKey, false);
|
||||
}
|
||||
|
||||
return success;
|
||||
return !!userKey;
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
const input = document.getElementById(this.pinLock ? "pin" : "masterPassword");
|
||||
const input = document.getElementById(this.pinEnabled ? "pin" : "masterPassword");
|
||||
if (this.ngZone.isStable) {
|
||||
input.focus();
|
||||
} else {
|
||||
@@ -152,25 +155,58 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
if (this.pinSet[0]) {
|
||||
const key = await this.cryptoService.makeKeyFromPin(
|
||||
let userKeyPin: EncString;
|
||||
let oldPinKey: EncString;
|
||||
switch (this.pinStatus) {
|
||||
case "PERSISTANT": {
|
||||
userKeyPin = await this.stateService.getPinKeyEncryptedUserKey();
|
||||
const oldEncryptedPinKey = await this.stateService.getEncryptedPinProtected();
|
||||
oldPinKey = oldEncryptedPinKey ? new EncString(oldEncryptedPinKey) : undefined;
|
||||
break;
|
||||
}
|
||||
case "TRANSIENT": {
|
||||
userKeyPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
||||
oldPinKey = await this.stateService.getDecryptedPinProtected();
|
||||
break;
|
||||
}
|
||||
case "DISABLED": {
|
||||
throw new Error("Pin is disabled");
|
||||
}
|
||||
default: {
|
||||
const _exhaustiveCheck: never = this.pinStatus;
|
||||
return _exhaustiveCheck;
|
||||
}
|
||||
}
|
||||
|
||||
let userKey: UserKey;
|
||||
if (oldPinKey) {
|
||||
userKey = await this.cryptoService.decryptAndMigrateOldPinKey(
|
||||
this.pinStatus === "TRANSIENT",
|
||||
this.pin,
|
||||
this.email,
|
||||
kdf,
|
||||
kdfConfig,
|
||||
await this.stateService.getDecryptedPinProtected()
|
||||
oldPinKey
|
||||
);
|
||||
const encKey = await this.cryptoService.getEncKey(key);
|
||||
const protectedPin = await this.stateService.getProtectedPin();
|
||||
const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey);
|
||||
failed = decPin !== this.pin;
|
||||
if (!failed) {
|
||||
await this.setKeyAndContinue(key);
|
||||
}
|
||||
} else {
|
||||
const key = await this.cryptoService.makeKeyFromPin(this.pin, this.email, kdf, kdfConfig);
|
||||
failed = false;
|
||||
await this.setKeyAndContinue(key);
|
||||
userKey = await this.cryptoService.decryptUserKeyWithPin(
|
||||
this.pin,
|
||||
this.email,
|
||||
kdf,
|
||||
kdfConfig,
|
||||
userKeyPin
|
||||
);
|
||||
}
|
||||
|
||||
const protectedPin = await this.stateService.getProtectedPin();
|
||||
const decryptedPin = await this.cryptoService.decryptToUtf8(
|
||||
new EncString(protectedPin),
|
||||
userKey
|
||||
);
|
||||
failed = decryptedPin !== this.pin;
|
||||
|
||||
if (!failed) {
|
||||
await this.setUserKeyAndContinue(userKey);
|
||||
}
|
||||
} catch {
|
||||
failed = true;
|
||||
@@ -206,18 +242,28 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
|
||||
const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfConfig);
|
||||
const storedKeyHash = await this.cryptoService.getKeyHash();
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
this.email,
|
||||
kdf,
|
||||
kdfConfig
|
||||
);
|
||||
const storedPasswordHash = await this.cryptoService.getMasterKeyHash();
|
||||
|
||||
let passwordValid = false;
|
||||
|
||||
if (storedKeyHash != null) {
|
||||
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(this.masterPassword, key);
|
||||
} else {
|
||||
const request = new SecretVerificationRequest();
|
||||
const serverKeyHash = await this.cryptoService.hashPassword(
|
||||
if (storedPasswordHash != null) {
|
||||
// Offline unlock possible
|
||||
passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
|
||||
this.masterPassword,
|
||||
key,
|
||||
masterKey
|
||||
);
|
||||
} else {
|
||||
// Online only
|
||||
const request = new SecretVerificationRequest();
|
||||
const serverKeyHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
masterKey,
|
||||
HashPurpose.ServerAuthorization
|
||||
);
|
||||
request.masterPasswordHash = serverKeyHash;
|
||||
@@ -226,14 +272,16 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
const response = await this.formPromise;
|
||||
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(response);
|
||||
passwordValid = true;
|
||||
const localKeyHash = await this.cryptoService.hashPassword(
|
||||
const localKeyHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
key,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
await this.cryptoService.setKeyHash(localKeyHash);
|
||||
await this.cryptoService.setMasterKeyHash(localKeyHash);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
} finally {
|
||||
this.formPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,19 +294,18 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.pinSet[0]) {
|
||||
const protectedPin = await this.stateService.getProtectedPin();
|
||||
const encKey = await this.cryptoService.getEncKey(key);
|
||||
const decPin = await this.cryptoService.decryptToUtf8(new EncString(protectedPin), encKey);
|
||||
const pinKey = await this.cryptoService.makePinKey(decPin, this.email, kdf, kdfConfig);
|
||||
await this.stateService.setDecryptedPinProtected(
|
||||
await this.cryptoService.encrypt(key.key, pinKey)
|
||||
);
|
||||
}
|
||||
await this.setKeyAndContinue(key, true);
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
await this.setUserKeyAndContinue(userKey, true);
|
||||
}
|
||||
private async setKeyAndContinue(key: SymmetricCryptoKey, evaluatePasswordAfterUnlock = false) {
|
||||
await this.cryptoService.setKey(key);
|
||||
|
||||
private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) {
|
||||
await this.cryptoService.setUserKey(key);
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
|
||||
await this.doContinue(evaluatePasswordAfterUnlock);
|
||||
}
|
||||
|
||||
@@ -296,24 +343,39 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async load() {
|
||||
this.pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
|
||||
this.pinLock =
|
||||
(this.pinSet[0] && (await this.stateService.getDecryptedPinProtected()) != null) ||
|
||||
this.pinSet[1];
|
||||
// TODO: Investigate PM-3515
|
||||
|
||||
// The loading of the lock component works as follows:
|
||||
// 1. First, is locking a valid timeout action? If not, we will log the user out.
|
||||
// 2. If locking IS a valid timeout action, we proceed to show the user the lock screen.
|
||||
// The user will be able to unlock as follows:
|
||||
// - If they have a PIN set, they will be presented with the PIN input
|
||||
// - If they have a master password and no PIN, they will be presented with the master password input
|
||||
// - If they have biometrics enabled, they will be presented with the biometric prompt
|
||||
|
||||
const availableVaultTimeoutActions = await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()
|
||||
);
|
||||
const supportsLock = availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock);
|
||||
if (!supportsLock) {
|
||||
return await this.vaultTimeoutService.logOut();
|
||||
}
|
||||
|
||||
this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
|
||||
|
||||
let ephemeralPinSet = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
||||
ephemeralPinSet ||= await this.stateService.getDecryptedPinProtected();
|
||||
this.pinEnabled =
|
||||
(this.pinStatus === "TRANSIENT" && !!ephemeralPinSet) || this.pinStatus === "PERSISTANT";
|
||||
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
|
||||
this.biometricLock =
|
||||
(await this.vaultTimeoutSettingsService.isBiometricLockSet()) &&
|
||||
((await this.cryptoService.hasKeyStored(KeySuffixOptions.Biometric)) ||
|
||||
((await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric)) ||
|
||||
!this.platformUtilsService.supportsSecureStorage());
|
||||
this.biometricText = await this.stateService.getBiometricText();
|
||||
this.email = await this.stateService.getEmail();
|
||||
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
|
||||
this.hideInput = usesKeyConnector && !this.pinLock;
|
||||
|
||||
// Users with key connector and without biometric or pin has no MP to unlock using
|
||||
if (usesKeyConnector && !(this.biometricLock || this.pinLock)) {
|
||||
await this.vaultTimeoutService.logOut();
|
||||
}
|
||||
|
||||
const webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
const vaultUrl =
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { IsActiveMatchOptions, Router } from "@angular/router";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { PasswordlessLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
|
||||
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-create-auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@@ -22,17 +28,24 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
// TODO: consider renaming this component something like LoginViaAuthReqComponent
|
||||
|
||||
enum State {
|
||||
StandardAuthRequest,
|
||||
AdminAuthRequest,
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class LoginWithDeviceComponent
|
||||
extends CaptchaProtectedComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private destroy$ = new Subject<void>();
|
||||
userAuthNStatus: AuthenticationStatus;
|
||||
email: string;
|
||||
showResendNotification = false;
|
||||
passwordlessRequest: PasswordlessCreateAuthRequest;
|
||||
@@ -42,12 +55,19 @@ export class LoginWithDeviceComponent
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
protected adminApprovalRoute = "admin-approval-requested";
|
||||
|
||||
protected StateEnum = State;
|
||||
protected state = State.StandardAuthRequest;
|
||||
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
private resendTimeout = 12000;
|
||||
private authRequestKeyPair: [publicKey: Uint8Array, privateKey: Uint8Array];
|
||||
|
||||
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
|
||||
|
||||
// TODO: in future, go to child components and remove child constructors and let deps fall through to the super class
|
||||
constructor(
|
||||
protected router: Router,
|
||||
private cryptoService: CryptoService,
|
||||
@@ -63,10 +83,14 @@ export class LoginWithDeviceComponent
|
||||
private anonymousHubService: AnonymousHubService,
|
||||
private validationService: ValidationService,
|
||||
private stateService: StateService,
|
||||
private loginService: LoginService
|
||||
private loginService: LoginService,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
private authReqCryptoService: AuthRequestCryptoServiceAbstraction
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
|
||||
// TODO: I don't know why this is necessary.
|
||||
// Why would the existence of the email depend on the navigation?
|
||||
const navigation = this.router.getCurrentNavigation();
|
||||
if (navigation) {
|
||||
this.email = this.loginService.getEmail();
|
||||
@@ -77,25 +101,167 @@ export class LoginWithDeviceComponent
|
||||
.getPushNotificationObs$()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => {
|
||||
this.confirmResponse(id);
|
||||
// Only fires on approval currently
|
||||
this.verifyAndHandleApprovedAuthReq(id);
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.email) {
|
||||
this.router.navigate(["/login"]);
|
||||
return;
|
||||
this.userAuthNStatus = await this.authService.getAuthStatus();
|
||||
|
||||
const matchOptions: IsActiveMatchOptions = {
|
||||
paths: "exact",
|
||||
queryParams: "ignored",
|
||||
fragment: "ignored",
|
||||
matrixParams: "ignored",
|
||||
};
|
||||
|
||||
if (this.router.isActive(this.adminApprovalRoute, matchOptions)) {
|
||||
this.state = State.AdminAuthRequest;
|
||||
}
|
||||
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
// Pull email from state for admin auth reqs b/c it is available
|
||||
// This also prevents it from being lost on refresh as the
|
||||
// login service email does not persist.
|
||||
this.email = await this.stateService.getEmail();
|
||||
|
||||
if (!this.email) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
|
||||
this.router.navigate(["/login-initiated"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// We only allow a single admin approval request to be active at a time
|
||||
// so must check state to see if we have an existing one or not
|
||||
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
|
||||
|
||||
if (adminAuthReqStorable) {
|
||||
await this.handleExistingAdminAuthRequest(adminAuthReqStorable);
|
||||
} else {
|
||||
// No existing admin auth request; so we need to create one
|
||||
await this.startPasswordlessLogin();
|
||||
}
|
||||
} else {
|
||||
// Standard auth request
|
||||
// TODO: evaluate if we can remove the setting of this.email in the constructor
|
||||
this.email = this.loginService.getEmail();
|
||||
|
||||
if (!this.email) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing"));
|
||||
this.router.navigate(["/login"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.startPasswordlessLogin();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.anonymousHubService.stopHubConnection();
|
||||
}
|
||||
|
||||
private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) {
|
||||
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
|
||||
// has been approved and handle it if so.
|
||||
|
||||
// Regardless, we always retrieve the auth request from the server verify and handle status changes here as well
|
||||
let adminAuthReqResponse: AuthRequestResponse;
|
||||
try {
|
||||
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied();
|
||||
}
|
||||
}
|
||||
|
||||
// Request doesn't exist anymore
|
||||
if (!adminAuthReqResponse) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied();
|
||||
}
|
||||
|
||||
// Re-derive the user's fingerprint phrase
|
||||
// It is important to not use the server's public key here as it could have been compromised via MITM
|
||||
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
|
||||
adminAuthReqStorable.privateKey
|
||||
);
|
||||
this.fingerprintPhrase = (
|
||||
await this.cryptoService.getFingerprint(this.email, derivedPublicKeyArrayBuffer)
|
||||
).join("-");
|
||||
|
||||
// Request denied
|
||||
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied();
|
||||
}
|
||||
|
||||
// Request approved
|
||||
if (adminAuthReqResponse.requestApproved) {
|
||||
return await this.handleApprovedAdminAuthRequest(
|
||||
adminAuthReqResponse,
|
||||
adminAuthReqStorable.privateKey
|
||||
);
|
||||
}
|
||||
|
||||
// Request still pending response from admin
|
||||
// So, create hub connection so that any approvals will be received via push notification
|
||||
this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
|
||||
}
|
||||
|
||||
private async handleExistingAdminAuthReqDeletedOrDenied() {
|
||||
// clear the admin auth request from state
|
||||
await this.stateService.setAdminAuthRequest(null);
|
||||
|
||||
// start new auth request
|
||||
this.startPasswordlessLogin();
|
||||
}
|
||||
|
||||
private async buildAuthRequest(authRequestType: AuthRequestType) {
|
||||
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
|
||||
this.authRequestKeyPair = {
|
||||
publicKey: authRequestKeyPairArray[0],
|
||||
privateKey: authRequestKeyPairArray[1],
|
||||
};
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
|
||||
|
||||
this.fingerprintPhrase = (
|
||||
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair.publicKey)
|
||||
).join("-");
|
||||
|
||||
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
|
||||
this.email,
|
||||
deviceIdentifier,
|
||||
publicKey,
|
||||
authRequestType,
|
||||
accessCode
|
||||
);
|
||||
}
|
||||
|
||||
async startPasswordlessLogin() {
|
||||
this.showResendNotification = false;
|
||||
|
||||
try {
|
||||
await this.buildAuthRequest();
|
||||
const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
|
||||
let reqResponse: AuthRequestResponse;
|
||||
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
await this.buildAuthRequest(AuthRequestType.AdminApproval);
|
||||
reqResponse = await this.apiService.postAdminAuthRequest(this.passwordlessRequest);
|
||||
|
||||
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
||||
id: reqResponse.id,
|
||||
privateKey: this.authRequestKeyPair.privateKey,
|
||||
});
|
||||
|
||||
await this.stateService.setAdminAuthRequest(adminAuthReqStorable);
|
||||
} else {
|
||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||
reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
|
||||
}
|
||||
|
||||
if (reqResponse.id) {
|
||||
this.anonymousHubService.createHubConnection(reqResponse.id);
|
||||
@@ -109,52 +275,69 @@ export class LoginWithDeviceComponent
|
||||
}, this.resendTimeout);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.anonymousHubService.stopHubConnection();
|
||||
}
|
||||
|
||||
private async confirmResponse(requestId: string) {
|
||||
private async verifyAndHandleApprovedAuthReq(requestId: string) {
|
||||
try {
|
||||
const response = await this.apiService.getAuthResponse(
|
||||
requestId,
|
||||
this.passwordlessRequest.accessCode
|
||||
);
|
||||
// Retrieve the auth request from server and verify it's approved
|
||||
let authReqResponse: AuthRequestResponse;
|
||||
|
||||
if (!response.requestApproved) {
|
||||
switch (this.state) {
|
||||
case State.StandardAuthRequest:
|
||||
// Unauthed - access code required for user verification
|
||||
authReqResponse = await this.apiService.getAuthResponse(
|
||||
requestId,
|
||||
this.passwordlessRequest.accessCode
|
||||
);
|
||||
break;
|
||||
|
||||
case State.AdminAuthRequest:
|
||||
// Authed - no access code required
|
||||
authReqResponse = await this.apiService.getAuthRequest(requestId);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!authReqResponse.requestApproved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = await this.buildLoginCredentials(requestId, response);
|
||||
const loginResponse = await this.authService.logIn(credentials);
|
||||
// Approved so proceed:
|
||||
|
||||
if (loginResponse.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.twoFactorRoute]);
|
||||
}
|
||||
} else if (loginResponse.forcePasswordReset != ForceResetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||
this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.forcePasswordResetRoute]);
|
||||
}
|
||||
} else {
|
||||
await this.setRememberEmailValues();
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
// 4 Scenarios to handle for approved auth requests:
|
||||
// Existing flow 1:
|
||||
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(masterKey)
|
||||
// > decrypt masterKey > must authenticate > gets masterKey(userKey) > decrypt userKey and proceed to vault
|
||||
|
||||
// 3 new flows from TDE:
|
||||
// Flow 2:
|
||||
// - Post SSO > User is AuthN > SSO login strategy success sets masterKey(userKey) > receives approval from device with pubKey(masterKey)
|
||||
// > decrypt masterKey > decrypt userKey > establish trust if required > proceed to vault
|
||||
// Flow 3:
|
||||
// - Post SSO > User is AuthN > Receives approval from device with pubKey(userKey) > decrypt userKey > establish trust if required > proceed to vault
|
||||
// Flow 4:
|
||||
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(userKey)
|
||||
// > decrypt userKey > must authenticate > set userKey > proceed to vault
|
||||
|
||||
// if user has authenticated via SSO
|
||||
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
|
||||
return await this.handleApprovedAdminAuthRequest(
|
||||
authReqResponse,
|
||||
this.authRequestKeyPair.privateKey
|
||||
);
|
||||
}
|
||||
|
||||
// Flow 1 and 4:
|
||||
const loginAuthResult = await this.loginViaPasswordlessStrategy(requestId, authReqResponse);
|
||||
await this.handlePostLoginNavigation(loginAuthResult);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.router.navigate(["/login"]);
|
||||
let errorRoute = "/login";
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
errorRoute = "/login-initiated";
|
||||
}
|
||||
|
||||
this.router.navigate([errorRoute]);
|
||||
this.validationService.showError(error);
|
||||
return;
|
||||
}
|
||||
@@ -163,6 +346,112 @@ export class LoginWithDeviceComponent
|
||||
}
|
||||
}
|
||||
|
||||
async handleApprovedAdminAuthRequest(
|
||||
adminAuthReqResponse: AuthRequestResponse,
|
||||
privateKey: ArrayBuffer
|
||||
) {
|
||||
// See verifyAndHandleApprovedAuthReq(...) for flow details
|
||||
// it's flow 2 or 3 based on presence of masterPasswordHash
|
||||
if (adminAuthReqResponse.masterPasswordHash) {
|
||||
// Flow 2: masterPasswordHash is not null
|
||||
// key is authRequestPublicKey(masterKey) + we have authRequestPublicKey(masterPasswordHash)
|
||||
await this.authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
adminAuthReqResponse,
|
||||
privateKey
|
||||
);
|
||||
} else {
|
||||
// Flow 3: masterPasswordHash is null
|
||||
// we can assume key is authRequestPublicKey(userKey) and we can just decrypt with userKey and proceed to vault
|
||||
await this.authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
|
||||
adminAuthReqResponse,
|
||||
privateKey
|
||||
);
|
||||
}
|
||||
|
||||
// clear the admin auth request from state so it cannot be used again (it's a one time use)
|
||||
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
||||
await this.stateService.setAdminAuthRequest(null);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
|
||||
// TODO: don't forget to use auto enrollment service everywhere we trust device
|
||||
|
||||
await this.handleSuccessfulLoginNavigation();
|
||||
}
|
||||
|
||||
// Authentication helper
|
||||
private async buildPasswordlessLoginCredentials(
|
||||
requestId: string,
|
||||
response: AuthRequestResponse
|
||||
): Promise<PasswordlessLogInCredentials> {
|
||||
// if masterPasswordHash has a value, we will always receive key as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
|
||||
// if masterPasswordHash is null, we will always receive key as authRequestPublicKey(userKey)
|
||||
if (response.masterPasswordHash) {
|
||||
const { masterKey, masterKeyHash } =
|
||||
await this.authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash(
|
||||
response.key,
|
||||
response.masterPasswordHash,
|
||||
this.authRequestKeyPair.privateKey
|
||||
);
|
||||
|
||||
return new PasswordlessLogInCredentials(
|
||||
this.email,
|
||||
this.passwordlessRequest.accessCode,
|
||||
requestId,
|
||||
null, // no userKey
|
||||
masterKey,
|
||||
masterKeyHash
|
||||
);
|
||||
} else {
|
||||
const userKey = await this.authReqCryptoService.decryptPubKeyEncryptedUserKey(
|
||||
response.key,
|
||||
this.authRequestKeyPair.privateKey
|
||||
);
|
||||
return new PasswordlessLogInCredentials(
|
||||
this.email,
|
||||
this.passwordlessRequest.accessCode,
|
||||
requestId,
|
||||
userKey,
|
||||
null, // no masterKey
|
||||
null // no masterKeyHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async loginViaPasswordlessStrategy(
|
||||
requestId: string,
|
||||
authReqResponse: AuthRequestResponse
|
||||
): Promise<AuthResult> {
|
||||
// Note: credentials change based on if the authReqResponse.key is a encryptedMasterKey or UserKey
|
||||
const credentials = await this.buildPasswordlessLoginCredentials(requestId, authReqResponse);
|
||||
|
||||
// Note: keys are set by PasswordlessLogInStrategy success handling
|
||||
return await this.authService.logIn(credentials);
|
||||
}
|
||||
|
||||
// Routing logic
|
||||
private async handlePostLoginNavigation(loginResponse: AuthResult) {
|
||||
if (loginResponse.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.twoFactorRoute]);
|
||||
}
|
||||
} else if (loginResponse.forcePasswordReset != ForceResetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||
this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.forcePasswordResetRoute]);
|
||||
}
|
||||
} else {
|
||||
await this.handleSuccessfulLoginNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
async setRememberEmailValues() {
|
||||
const rememberEmail = this.loginService.getRememberEmail();
|
||||
const rememberedEmail = this.loginService.getEmail();
|
||||
@@ -170,43 +459,19 @@ export class LoginWithDeviceComponent
|
||||
this.loginService.clearValues();
|
||||
}
|
||||
|
||||
private async buildAuthRequest() {
|
||||
this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
|
||||
private async handleSuccessfulLoginNavigation() {
|
||||
if (this.state === State.StandardAuthRequest) {
|
||||
// Only need to set remembered email on standard login with auth req flow
|
||||
await this.setRememberEmailValues();
|
||||
}
|
||||
|
||||
this.fingerprintPhrase = (
|
||||
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0])
|
||||
).join("-");
|
||||
|
||||
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
|
||||
this.email,
|
||||
deviceIdentifier,
|
||||
publicKey,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
accessCode
|
||||
);
|
||||
}
|
||||
|
||||
private async buildLoginCredentials(
|
||||
requestId: string,
|
||||
response: AuthRequestResponse
|
||||
): Promise<PasswordlessLogInCredentials> {
|
||||
const decKey = await this.cryptoService.rsaDecrypt(response.key, this.authRequestKeyPair[1]);
|
||||
const decMasterPasswordHash = await this.cryptoService.rsaDecrypt(
|
||||
response.masterPasswordHash,
|
||||
this.authRequestKeyPair[1]
|
||||
);
|
||||
const key = new SymmetricCryptoKey(decKey);
|
||||
const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash);
|
||||
|
||||
return new PasswordlessLogInCredentials(
|
||||
this.email,
|
||||
this.passwordlessRequest.accessCode,
|
||||
requestId,
|
||||
key,
|
||||
localHashedPassword
|
||||
);
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
|
||||
@@ -8,8 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class RemovePasswordComponent implements OnInit {
|
||||
@@ -29,7 +28,7 @@ export class RemovePasswordComponent implements OnInit {
|
||||
private i18nService: I18nService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private dialogService: DialogServiceAbstraction
|
||||
private dialogService: DialogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -61,7 +60,7 @@ export class RemovePasswordComponent implements OnInit {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.organization.name,
|
||||
content: { key: "leaveOrganizationConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
567
libs/angular/src/auth/components/sso.component.spec.ts
Normal file
567
libs/angular/src/auth/components/sso.component.spec.ts
Normal file
@@ -0,0 +1,567 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
import { SsoComponent } from "./sso.component";
|
||||
// test component that extends the SsoComponent
|
||||
@Component({})
|
||||
class TestSsoComponent extends SsoComponent {}
|
||||
|
||||
interface SsoComponentProtected {
|
||||
twoFactorRoute: string;
|
||||
successRoute: string;
|
||||
trustedDeviceEncRoute: string;
|
||||
changePasswordRoute: string;
|
||||
forcePasswordResetRoute: string;
|
||||
logIn(code: string, codeVerifier: string, orgIdFromState: string): Promise<AuthResult>;
|
||||
handleLoginError(e: any): Promise<void>;
|
||||
}
|
||||
|
||||
// The ideal scenario would be to not have to test the protected / private methods of the SsoComponent
|
||||
// but that will require a refactor of the SsoComponent class which is out of scope for now.
|
||||
// This test suite allows us to be sure that the new Trusted Device encryption flows + mild refactors
|
||||
// of the SsoComponent don't break the existing post login flows.
|
||||
describe("SsoComponent", () => {
|
||||
let component: TestSsoComponent;
|
||||
let _component: SsoComponentProtected;
|
||||
let fixture: ComponentFixture<TestSsoComponent>;
|
||||
|
||||
// Mock Services
|
||||
let mockAuthService: MockProxy<AuthService>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
let mockQueryParams: Observable<any>;
|
||||
let mockActivatedRoute: ActivatedRoute;
|
||||
|
||||
let mockStateService: MockProxy<StateService>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
|
||||
// Mock authService.logIn params
|
||||
let code: string;
|
||||
let codeVerifier: string;
|
||||
let orgIdFromState: string;
|
||||
|
||||
// Mock component callbacks
|
||||
let mockOnSuccessfulLogin: jest.Mock;
|
||||
let mockOnSuccessfulLoginNavigate: jest.Mock;
|
||||
let mockOnSuccessfulLoginTwoFactorNavigate: jest.Mock;
|
||||
let mockOnSuccessfulLoginChangePasswordNavigate: jest.Mock;
|
||||
let mockOnSuccessfulLoginForceResetNavigate: jest.Mock;
|
||||
let mockOnSuccessfulLoginTdeNavigate: jest.Mock;
|
||||
|
||||
let mockAcctDecryptionOpts: {
|
||||
noMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Services
|
||||
mockAuthService = mock<AuthService>();
|
||||
mockRouter = mock<Router>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
// Default mockQueryParams
|
||||
mockQueryParams = of({ code: "code", state: "state" });
|
||||
// Create a custom mock for ActivatedRoute with mock queryParams
|
||||
mockActivatedRoute = {
|
||||
queryParams: mockQueryParams,
|
||||
} as any as ActivatedRoute;
|
||||
|
||||
mockStateService = mock<StateService>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockCryptoFunctionService = mock<CryptoFunctionService>();
|
||||
mockEnvironmentService = mock<EnvironmentService>();
|
||||
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
|
||||
// Mock authService.logIn params
|
||||
code = "code";
|
||||
codeVerifier = "codeVerifier";
|
||||
orgIdFromState = "orgIdFromState";
|
||||
|
||||
// Mock component callbacks
|
||||
mockOnSuccessfulLogin = jest.fn();
|
||||
mockOnSuccessfulLoginNavigate = jest.fn();
|
||||
mockOnSuccessfulLoginTwoFactorNavigate = jest.fn();
|
||||
mockOnSuccessfulLoginChangePasswordNavigate = jest.fn();
|
||||
mockOnSuccessfulLoginForceResetNavigate = jest.fn();
|
||||
mockOnSuccessfulLoginTdeNavigate = jest.fn();
|
||||
|
||||
mockAcctDecryptionOpts = {
|
||||
noMasterPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestSsoComponent],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
{ provide: StateService, useValue: mockStateService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: CryptoFunctionService, useValue: mockCryptoFunctionService },
|
||||
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService },
|
||||
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TestSsoComponent);
|
||||
component = fixture.componentInstance;
|
||||
_component = component as any;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset all mocks after each test
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("navigateViaCallbackOrRoute(...)", () => {
|
||||
it("calls the provided callback when callback is defined", async () => {
|
||||
const callback = jest.fn().mockResolvedValue(null);
|
||||
const commands = ["some", "route"];
|
||||
|
||||
await (component as any).navigateViaCallbackOrRoute(callback, commands);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls router.navigate when callback is not defined", async () => {
|
||||
const commands = ["some", "route"];
|
||||
|
||||
await (component as any).navigateViaCallbackOrRoute(undefined, commands);
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(commands, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("logIn(...)", () => {
|
||||
describe("2FA scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const authResult = new AuthResult();
|
||||
authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]);
|
||||
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword
|
||||
);
|
||||
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
it("calls authService.logIn and navigates to the component's defined 2FA route when the auth result requires 2FA and onSuccessfulLoginTwoFactorNavigate is not defined", async () => {
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnSuccessfulLoginTwoFactorNavigate).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.twoFactorRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdFromState,
|
||||
sso: "true",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginTwoFactorNavigate instead of router.navigate when response.requiresTwoFactor is true and the callback is defined", async () => {
|
||||
mockOnSuccessfulLoginTwoFactorNavigate = jest.fn().mockResolvedValue(null);
|
||||
component.onSuccessfulLoginTwoFactorNavigate = mockOnSuccessfulLoginTwoFactorNavigate;
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnSuccessfulLoginTwoFactorNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// Shared test helpers
|
||||
const testChangePasswordOnSuccessfulLogin = () => {
|
||||
it("navigates to the component's defined change password route when onSuccessfulLoginChangePasswordNavigate callback is undefined", async () => {
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnSuccessfulLoginChangePasswordNavigate).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdFromState,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
const testOnSuccessfulLoginChangePasswordNavigate = () => {
|
||||
it("calls onSuccessfulLoginChangePasswordNavigate instead of router.navigate when the callback is defined", async () => {
|
||||
mockOnSuccessfulLoginChangePasswordNavigate = jest.fn().mockResolvedValue(null);
|
||||
component.onSuccessfulLoginChangePasswordNavigate =
|
||||
mockOnSuccessfulLoginChangePasswordNavigate;
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnSuccessfulLoginChangePasswordNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
const testForceResetOnSuccessfulLogin = (reasonString: string) => {
|
||||
it(`navigates to the component's defined forcePasswordResetRoute when response.forcePasswordReset is ${reasonString}`, async () => {
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnSuccessfulLoginForceResetNavigate).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.forcePasswordResetRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdFromState,
|
||||
},
|
||||
});
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
const testOnSuccessfulLoginForceResetNavigate = (reasonString: string) => {
|
||||
it(`calls onSuccessfulLoginForceResetNavigate instead of router.navigate when response.forcePasswordReset is ${reasonString} and the callback is defined`, async () => {
|
||||
mockOnSuccessfulLoginForceResetNavigate = jest.fn().mockResolvedValue(null);
|
||||
component.onSuccessfulLoginForceResetNavigate = mockOnSuccessfulLoginForceResetNavigate;
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnSuccessfulLoginForceResetNavigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
describe("Trusted Device Encryption scenarios", () => {
|
||||
beforeEach(() => {
|
||||
mockConfigService.getFeatureFlagBool.mockResolvedValue(true); // TDE enabled
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
testOnSuccessfulLoginChangePasswordNavigate();
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
|
||||
[
|
||||
ForceResetPasswordReason.AdminForcePasswordReset,
|
||||
ForceResetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
testOnSuccessfulLoginForceResetNavigate(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = ForceResetPasswordReason.None;
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
it("navigates to the component's defined trusted device encryption route when login is successful and no callback is defined", async () => {
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||
[_component.trustedDeviceEncRoute],
|
||||
undefined
|
||||
);
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginTdeNavigate instead of router.navigate when the callback is defined", async () => {
|
||||
mockOnSuccessfulLoginTdeNavigate = jest.fn().mockResolvedValue(null);
|
||||
component.onSuccessfulLoginTdeNavigate = mockOnSuccessfulLoginTdeNavigate;
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnSuccessfulLoginTdeNavigate).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Set Master Password scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const authResult = new AuthResult();
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
describe("Given user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
// Only need to test the case where the user has no master password to test the primary change mp flow here
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPassword
|
||||
);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
testOnSuccessfulLoginChangePasswordNavigate();
|
||||
});
|
||||
|
||||
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector
|
||||
);
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
expect(mockAuthService.logIn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnSuccessfulLoginChangePasswordNavigate).not.toHaveBeenCalled();
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdFromState,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Force Master Password Reset scenarios", () => {
|
||||
[
|
||||
ForceResetPasswordReason.AdminForcePasswordReset,
|
||||
ForceResetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
testOnSuccessfulLoginForceResetNavigate(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Success scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const authResult = new AuthResult();
|
||||
authResult.twoFactorProviders = null;
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword
|
||||
);
|
||||
authResult.forcePasswordReset = ForceResetPasswordReason.None;
|
||||
mockAuthService.logIn.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
it("calls authService.logIn and navigates to the component's defined success route when the login is successful", async () => {
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalled();
|
||||
|
||||
expect(mockOnSuccessfulLoginNavigate).not.toHaveBeenCalled();
|
||||
expect(mockOnSuccessfulLogin).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.successRoute], undefined);
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLogin if defined when login is successful", async () => {
|
||||
mockOnSuccessfulLogin = jest.fn().mockResolvedValue(null);
|
||||
component.onSuccessfulLogin = mockOnSuccessfulLogin;
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalled();
|
||||
expect(mockOnSuccessfulLogin).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockOnSuccessfulLoginNavigate).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.successRoute], undefined);
|
||||
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginNavigate instead of router.navigate when login is successful and the callback is defined", async () => {
|
||||
mockOnSuccessfulLoginNavigate = jest.fn().mockResolvedValue(null);
|
||||
component.onSuccessfulLoginNavigate = mockOnSuccessfulLoginNavigate;
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(mockAuthService.logIn).toHaveBeenCalled();
|
||||
|
||||
expect(mockOnSuccessfulLoginNavigate).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
|
||||
expect(mockLogService.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error scenarios", () => {
|
||||
it("calls handleLoginError when an error is thrown during logIn", async () => {
|
||||
const errorMessage = "Key Connector error";
|
||||
const error = new Error(errorMessage);
|
||||
mockAuthService.logIn.mockRejectedValue(error);
|
||||
|
||||
const handleLoginErrorSpy = jest.spyOn(_component, "handleLoginError");
|
||||
|
||||
await _component.logIn(code, codeVerifier, orgIdFromState);
|
||||
|
||||
expect(handleLoginErrorSpy).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleLoginError(e)", () => {
|
||||
it("logs the error and shows a toast when the error message is 'Key Connector error'", async () => {
|
||||
const errorMessage = "Key Connector error";
|
||||
const error = new Error(errorMessage);
|
||||
|
||||
mockI18nService.t.mockReturnValueOnce("ssoKeyConnectorError");
|
||||
|
||||
await _component.handleLoginError(error);
|
||||
|
||||
expect(mockLogService.error).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogService.error).toHaveBeenCalledWith(error);
|
||||
|
||||
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledTimes(1);
|
||||
expect(mockPlatformUtilsService.showToast).toHaveBeenCalledWith(
|
||||
"error",
|
||||
null,
|
||||
"ssoKeyConnectorError"
|
||||
);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Directive } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -7,7 +7,10 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { SsoLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -15,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
|
||||
@Directive()
|
||||
@@ -24,14 +28,18 @@ export class SsoComponent {
|
||||
|
||||
formPromise: Promise<AuthResult>;
|
||||
initiateSsoFormPromise: Promise<SsoPreValidateResponse>;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginChangePasswordNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
onSuccessfulLogin: () => Promise<void>;
|
||||
onSuccessfulLoginNavigate: () => Promise<void>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<void>;
|
||||
onSuccessfulLoginChangePasswordNavigate: () => Promise<void>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<void>;
|
||||
|
||||
onSuccessfulLoginTde: () => Promise<void>;
|
||||
onSuccessfulLoginTdeNavigate: () => Promise<void>;
|
||||
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "lock";
|
||||
protected trustedDeviceEncRoute = "login-initiated";
|
||||
protected changePasswordRoute = "set-password";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected clientId: string;
|
||||
@@ -50,7 +58,8 @@ export class SsoComponent {
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected logService: LogService
|
||||
protected logService: LogService,
|
||||
protected configService: ConfigServiceAbstraction
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -67,11 +76,12 @@ export class SsoComponent {
|
||||
state != null &&
|
||||
this.checkState(state, qParams.state)
|
||||
) {
|
||||
await this.logIn(
|
||||
qParams.code,
|
||||
codeVerifier,
|
||||
this.getOrgIdentifierFromState(qParams.state)
|
||||
);
|
||||
// We are not using a query param to pass org identifier around specifically
|
||||
// for the browser SSO case when it needs it on extension open after SSO success
|
||||
// on the TDE login decryption options component
|
||||
const ssoOrganizationIdentifier = this.getOrgIdentifierFromState(qParams.state);
|
||||
await this.logIn(qParams.code, codeVerifier, ssoOrganizationIdentifier);
|
||||
await this.stateService.setUserSsoOrganizationIdentifier(ssoOrganizationIdentifier);
|
||||
}
|
||||
} else if (
|
||||
qParams.clientId != null &&
|
||||
@@ -173,67 +183,172 @@ export class SsoComponent {
|
||||
return authorizeUrl;
|
||||
}
|
||||
|
||||
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
|
||||
private async logIn(code: string, codeVerifier: string, orgIdentifier: string) {
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
const credentials = new SsoLogInCredentials(
|
||||
code,
|
||||
codeVerifier,
|
||||
this.redirectUri,
|
||||
orgIdFromState
|
||||
orgIdentifier
|
||||
);
|
||||
this.formPromise = this.authService.logIn(credentials);
|
||||
const response = await this.formPromise;
|
||||
if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
await this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.twoFactorRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdFromState,
|
||||
sso: "true",
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (response.resetMasterPassword) {
|
||||
if (this.onSuccessfulLoginChangePasswordNavigate != null) {
|
||||
await this.onSuccessfulLoginChangePasswordNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdFromState,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (response.forcePasswordReset !== ForceResetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||
await this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.forcePasswordResetRoute]);
|
||||
}
|
||||
} else {
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
await this.onSuccessfulLogin();
|
||||
}
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
await this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
const authResult = await this.formPromise;
|
||||
|
||||
// TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here
|
||||
if (e.message === "Key Connector error") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("ssoKeyConnectorError")
|
||||
const acctDecryptionOpts: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
|
||||
if (authResult.requiresTwoFactor) {
|
||||
return await this.handleTwoFactorRequired(orgIdentifier);
|
||||
}
|
||||
|
||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(
|
||||
acctDecryptionOpts.trustedDeviceOption
|
||||
);
|
||||
|
||||
if (tdeEnabled) {
|
||||
return await this.handleTrustedDeviceEncryptionEnabled(
|
||||
authResult,
|
||||
orgIdentifier,
|
||||
acctDecryptionOpts
|
||||
);
|
||||
}
|
||||
|
||||
// In the standard, non TDE case, a user must set password if they don't
|
||||
// have one and they aren't using key connector.
|
||||
// Note: TDE & Key connector are mutually exclusive org config options.
|
||||
const requireSetPassword =
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
acctDecryptionOpts.keyConnectorOption === undefined;
|
||||
|
||||
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||
// Change implies going no password -> password in this case
|
||||
return await this.handleChangePasswordRequired(orgIdentifier);
|
||||
}
|
||||
|
||||
// Users can be forced to reset their password via an admin or org policy
|
||||
// disallowing weak passwords
|
||||
if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
|
||||
return await this.handleForcePasswordReset(orgIdentifier);
|
||||
}
|
||||
|
||||
// Standard SSO login success case
|
||||
return await this.handleSuccessfulLogin();
|
||||
} catch (e) {
|
||||
await this.handleLoginError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async isTrustedDeviceEncEnabled(
|
||||
trustedDeviceOption: TrustedDeviceUserDecryptionOption
|
||||
): Promise<boolean> {
|
||||
const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool(
|
||||
FeatureFlag.TrustedDeviceEncryption
|
||||
);
|
||||
|
||||
return trustedDeviceEncryptionFeatureActive && trustedDeviceOption !== undefined;
|
||||
}
|
||||
|
||||
private async handleTwoFactorRequired(orgIdentifier: string) {
|
||||
await this.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginTwoFactorNavigate,
|
||||
[this.twoFactorRoute],
|
||||
{
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
sso: "true",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async handleTrustedDeviceEncryptionEnabled(
|
||||
authResult: AuthResult,
|
||||
orgIdentifier: string,
|
||||
acctDecryptionOpts: AccountDecryptionOptions
|
||||
): Promise<void> {
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
) {
|
||||
// Change implies going no password -> password in this case
|
||||
return await this.handleChangePasswordRequired(orgIdentifier);
|
||||
}
|
||||
|
||||
if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
|
||||
return await this.handleForcePasswordReset(orgIdentifier);
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginTde != null) {
|
||||
// Don't await b/c causes hang on desktop & browser
|
||||
this.onSuccessfulLoginTde();
|
||||
}
|
||||
|
||||
this.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginTdeNavigate,
|
||||
// Navigate to TDE page (if user was on trusted device and TDE has decrypted
|
||||
// their user key, the login-initiated guard will redirect them to the vault)
|
||||
[this.trustedDeviceEncRoute]
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||
await this.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginChangePasswordNavigate,
|
||||
[this.changePasswordRoute],
|
||||
{
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async handleForcePasswordReset(orgIdentifier: string) {
|
||||
await this.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginForceResetNavigate,
|
||||
[this.forcePasswordResetRoute],
|
||||
{
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSuccessfulLogin() {
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
// Don't await b/c causes hang on desktop & browser
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
|
||||
await this.navigateViaCallbackOrRoute(this.onSuccessfulLoginNavigate, [this.successRoute]);
|
||||
}
|
||||
|
||||
private async handleLoginError(e: any) {
|
||||
this.logService.error(e);
|
||||
|
||||
// TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here
|
||||
if (e.message === "Key Connector error") {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("ssoKeyConnectorError")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async navigateViaCallbackOrRoute(
|
||||
callback: () => Promise<unknown>,
|
||||
commands: unknown[],
|
||||
extras?: NavigationExtras
|
||||
): Promise<void> {
|
||||
if (callback) {
|
||||
await callback();
|
||||
} else {
|
||||
await this.router.navigate(commands, extras);
|
||||
}
|
||||
this.loggingIn = false;
|
||||
}
|
||||
|
||||
private getOrgIdentifierFromState(state: string): string {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class TwoFactorOptionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
recover() {
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/help/lost-two-step-device/");
|
||||
this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/recover-2fa");
|
||||
this.onRecoverSelected.emit();
|
||||
}
|
||||
}
|
||||
|
||||
451
libs/angular/src/auth/components/two-factor.component.spec.ts
Normal file
451
libs/angular/src/auth/components/two-factor.component.spec.ts
Normal file
@@ -0,0 +1,451 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
import { TwoFactorComponent } from "./two-factor.component";
|
||||
|
||||
// test component that extends the TwoFactorComponent
|
||||
@Component({})
|
||||
class TestTwoFactorComponent extends TwoFactorComponent {}
|
||||
|
||||
interface TwoFactorComponentProtected {
|
||||
trustedDeviceEncRoute: string;
|
||||
changePasswordRoute: string;
|
||||
forcePasswordResetRoute: string;
|
||||
successRoute: string;
|
||||
}
|
||||
|
||||
describe("TwoFactorComponent", () => {
|
||||
let component: TestTwoFactorComponent;
|
||||
let _component: TwoFactorComponentProtected;
|
||||
|
||||
let fixture: ComponentFixture<TestTwoFactorComponent>;
|
||||
|
||||
// Mock Services
|
||||
let mockAuthService: MockProxy<AuthService>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let mockWin: MockProxy<Window>;
|
||||
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||
let mockStateService: MockProxy<StateService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockTwoFactorService: MockProxy<TwoFactorService>;
|
||||
let mockAppIdService: MockProxy<AppIdService>;
|
||||
let mockLoginService: MockProxy<LoginService>;
|
||||
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||
|
||||
let mockAcctDecryptionOpts: {
|
||||
noMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDevice: AccountDecryptionOptions;
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
withMasterPasswordAndKeyConnector: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDevice: AccountDecryptionOptions;
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions;
|
||||
noMasterPasswordWithKeyConnector: AccountDecryptionOptions;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockAuthService = mock<AuthService>();
|
||||
mockRouter = mock<Router>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
mockWin = mock<Window>();
|
||||
mockEnvironmentService = mock<EnvironmentService>();
|
||||
mockStateService = mock<StateService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockTwoFactorService = mock<TwoFactorService>();
|
||||
mockAppIdService = mock<AppIdService>();
|
||||
mockLoginService = mock<LoginService>();
|
||||
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||
|
||||
mockAcctDecryptionOpts = {
|
||||
noMasterPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({
|
||||
hasMasterPassword: true,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true),
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({
|
||||
hasMasterPassword: false,
|
||||
trustedDeviceOption: undefined,
|
||||
keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"),
|
||||
}),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestTwoFactorComponent],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ApiService, useValue: mockApiService },
|
||||
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||
{ provide: WINDOW, useValue: mockWin },
|
||||
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||
{ provide: StateService, useValue: mockStateService },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
snapshot: {
|
||||
// Default to standard 2FA flow - not SSO + 2FA
|
||||
queryParamMap: convertToParamMap({ sso: "false" }),
|
||||
},
|
||||
},
|
||||
},
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: TwoFactorService, useValue: mockTwoFactorService },
|
||||
{ provide: AppIdService, useValue: mockAppIdService },
|
||||
{ provide: LoginService, useValue: mockLoginService },
|
||||
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||
],
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TestTwoFactorComponent);
|
||||
component = fixture.componentInstance;
|
||||
_component = component as any;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset all mocks after each test
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
// Shared tests
|
||||
const testChangePasswordOnSuccessfulLogin = () => {
|
||||
it("navigates to the component's defined change password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: component.orgIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const testForceResetOnSuccessfulLogin = (reasonString: string) => {
|
||||
it(`navigates to the component's defined forcePasswordResetRoute route when response.forcePasswordReset is ${reasonString}`, async () => {
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.forcePasswordResetRoute], {
|
||||
queryParams: {
|
||||
identifier: component.orgIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe("Standard 2FA scenarios", () => {
|
||||
describe("doSubmit", () => {
|
||||
const token = "testToken";
|
||||
const remember = false;
|
||||
const captchaToken = "testCaptchaToken";
|
||||
|
||||
beforeEach(() => {
|
||||
component.token = token;
|
||||
component.remember = remember;
|
||||
component.captchaToken = captchaToken;
|
||||
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword
|
||||
);
|
||||
});
|
||||
|
||||
it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => {
|
||||
// Arrange
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(mockAuthService.logInTwoFactor).toHaveBeenCalledWith(
|
||||
new TokenTwoFactorRequest(component.selectedProviderType, token, remember),
|
||||
captchaToken
|
||||
);
|
||||
});
|
||||
|
||||
it("should return when handleCaptchaRequired returns true", async () => {
|
||||
// Arrange
|
||||
const captchaSiteKey = "testCaptchaSiteKey";
|
||||
const authResult = new AuthResult();
|
||||
authResult.captchaSiteKey = captchaSiteKey;
|
||||
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
|
||||
// Note: the any casts are required b/c typescript cant recognize that
|
||||
// handleCaptureRequired is a method on TwoFactorComponent b/c it is inherited
|
||||
// from the CaptchaProtectedComponent
|
||||
const handleCaptchaRequiredSpy = jest
|
||||
.spyOn<any, any>(component, "handleCaptchaRequired")
|
||||
.mockReturnValue(true);
|
||||
|
||||
// Act
|
||||
const result = await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(handleCaptchaRequiredSpy).toHaveBeenCalled();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLogin when defined", async () => {
|
||||
// Arrange
|
||||
component.onSuccessfulLogin = jest.fn().mockResolvedValue(undefined);
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(component.onSuccessfulLogin).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls loginService.clearValues() when login is successful", async () => {
|
||||
// Arrange
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
// spy on loginService.clearValues
|
||||
const clearValuesSpy = jest.spyOn(mockLoginService, "clearValues");
|
||||
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(clearValuesSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Set Master Password scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const authResult = new AuthResult();
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
describe("Given user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
// Only need to test the case where the user has no master password to test the primary change mp flow here
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPassword
|
||||
);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
});
|
||||
|
||||
it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector
|
||||
);
|
||||
|
||||
await component.doSubmit();
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith([_component.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: component.orgIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Force Master Password Reset scenarios", () => {
|
||||
[
|
||||
ForceResetPasswordReason.AdminForcePasswordReset,
|
||||
ForceResetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPassword
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginNavigate when the callback is defined", async () => {
|
||||
// Arrange
|
||||
component.onSuccessfulLoginNavigate = jest.fn().mockResolvedValue(undefined);
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(component.onSuccessfulLoginNavigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("navigates to the component's defined success route when the login is successful and onSuccessfulLoginNavigate is undefined", async () => {
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
// Act
|
||||
await component.doSubmit();
|
||||
|
||||
// Assert
|
||||
expect(component.onSuccessfulLoginNavigate).not.toBeDefined();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith([_component.successRoute], undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SSO > 2FA scenarios", () => {
|
||||
beforeEach(() => {
|
||||
const mockActivatedRoute = TestBed.inject(ActivatedRoute);
|
||||
mockActivatedRoute.snapshot.queryParamMap.get = jest.fn().mockReturnValue("true");
|
||||
});
|
||||
|
||||
describe("doSubmit", () => {
|
||||
const token = "testToken";
|
||||
const remember = false;
|
||||
const captchaToken = "testCaptchaToken";
|
||||
|
||||
beforeEach(() => {
|
||||
component.token = token;
|
||||
component.remember = remember;
|
||||
component.captchaToken = captchaToken;
|
||||
});
|
||||
|
||||
describe("Trusted Device Encryption scenarios", () => {
|
||||
beforeEach(() => {
|
||||
mockConfigService.getFeatureFlagBool.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => {
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testChangePasswordOnSuccessfulLogin();
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
|
||||
[
|
||||
ForceResetPasswordReason.AdminForcePasswordReset,
|
||||
ForceResetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceResetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
mockStateService.getAccountDecryptionOptions.mockResolvedValue(
|
||||
mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = ForceResetPasswordReason.None;
|
||||
mockAuthService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
it("navigates to the component's defined trusted device encryption route when login is successful and onSuccessfulLoginTdeNavigate is undefined", async () => {
|
||||
await component.doSubmit();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(
|
||||
[_component.trustedDeviceEncRoute],
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoginTdeNavigate instead of router.navigate when the callback is defined", async () => {
|
||||
component.onSuccessfulLoginTdeNavigate = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await component.doSubmit();
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalled();
|
||||
expect(component.onSuccessfulLoginTdeNavigate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Directive, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import * as DuoWebSDK from "duo_web_sdk";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
@@ -10,16 +12,20 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
|
||||
import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@@ -38,11 +44,18 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
twoFactorEmail: string = null;
|
||||
formPromise: Promise<any>;
|
||||
emailPromise: Promise<any>;
|
||||
identifier: string = null;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
orgIdentifier: string = null;
|
||||
onSuccessfulLogin: () => Promise<void>;
|
||||
onSuccessfulLoginNavigate: () => Promise<void>;
|
||||
|
||||
onSuccessfulLoginTde: () => Promise<void>;
|
||||
onSuccessfulLoginTdeNavigate: () => Promise<void>;
|
||||
|
||||
protected loginRoute = "login";
|
||||
|
||||
protected trustedDeviceEncRoute = "login-initiated";
|
||||
protected changePasswordRoute = "set-password";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
protected successRoute = "vault";
|
||||
|
||||
constructor(
|
||||
@@ -51,14 +64,15 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected i18nService: I18nService,
|
||||
protected apiService: ApiService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected win: Window,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected stateService: StateService,
|
||||
protected route: ActivatedRoute,
|
||||
protected logService: LogService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected appIdService: AppIdService,
|
||||
protected loginService: LoginService
|
||||
protected loginService: LoginService,
|
||||
protected configService: ConfigServiceAbstraction
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
@@ -72,7 +86,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
|
||||
this.route.queryParams.pipe(first()).subscribe((qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
this.identifier = qParams.identifier;
|
||||
this.orgIdentifier = qParams.identifier;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,32 +210,132 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
new TokenTwoFactorRequest(this.selectedProviderType, this.token, this.remember),
|
||||
this.captchaToken
|
||||
);
|
||||
const response: AuthResult = await this.formPromise;
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
const authResult: AuthResult = await this.formPromise;
|
||||
|
||||
await this.handleLoginResponse(authResult);
|
||||
}
|
||||
|
||||
private async handleLoginResponse(authResult: AuthResult) {
|
||||
if (this.handleCaptchaRequired(authResult)) {
|
||||
return;
|
||||
}
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
this.loginService.clearValues();
|
||||
|
||||
this.loginService.clearValues();
|
||||
|
||||
const acctDecryptionOpts: AccountDecryptionOptions =
|
||||
await this.stateService.getAccountDecryptionOptions();
|
||||
|
||||
const tdeEnabled = await this.isTrustedDeviceEncEnabled(acctDecryptionOpts.trustedDeviceOption);
|
||||
|
||||
if (tdeEnabled) {
|
||||
return await this.handleTrustedDeviceEncryptionEnabled(
|
||||
authResult,
|
||||
this.orgIdentifier,
|
||||
acctDecryptionOpts
|
||||
);
|
||||
}
|
||||
|
||||
// User must set password if they don't have one and they aren't using either TDE or key connector.
|
||||
const requireSetPassword =
|
||||
!acctDecryptionOpts.hasMasterPassword && acctDecryptionOpts.keyConnectorOption === undefined;
|
||||
|
||||
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||
// Change implies going no password -> password in this case
|
||||
return await this.handleChangePasswordRequired(this.orgIdentifier);
|
||||
}
|
||||
|
||||
// Users can be forced to reset their password via an admin or org policy
|
||||
// disallowing weak passwords
|
||||
if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
|
||||
return await this.handleForcePasswordReset(this.orgIdentifier);
|
||||
}
|
||||
|
||||
return await this.handleSuccessfulLogin();
|
||||
}
|
||||
|
||||
private async isTrustedDeviceEncEnabled(
|
||||
trustedDeviceOption: TrustedDeviceUserDecryptionOption
|
||||
): Promise<boolean> {
|
||||
const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true";
|
||||
const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool(
|
||||
FeatureFlag.TrustedDeviceEncryption
|
||||
);
|
||||
|
||||
return (
|
||||
ssoTo2faFlowActive &&
|
||||
trustedDeviceEncryptionFeatureActive &&
|
||||
trustedDeviceOption !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
private async handleTrustedDeviceEncryptionEnabled(
|
||||
authResult: AuthResult,
|
||||
orgIdentifier: string,
|
||||
acctDecryptionOpts: AccountDecryptionOptions
|
||||
): Promise<void> {
|
||||
// If user doesn't have a MP, but has reset password permission, they must set a MP
|
||||
if (
|
||||
!acctDecryptionOpts.hasMasterPassword &&
|
||||
acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
) {
|
||||
// Change implies going no password -> password in this case
|
||||
return await this.handleChangePasswordRequired(orgIdentifier);
|
||||
}
|
||||
|
||||
// Users can be forced to reset their password via an admin or org policy
|
||||
// disallowing weak passwords
|
||||
if (authResult.forcePasswordReset !== ForceResetPasswordReason.None) {
|
||||
return await this.handleForcePasswordReset(orgIdentifier);
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginTde != null) {
|
||||
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||
// before nagivating to the success route.
|
||||
// before navigating to the success route.
|
||||
this.onSuccessfulLoginTde();
|
||||
}
|
||||
|
||||
this.navigateViaCallbackOrRoute(
|
||||
this.onSuccessfulLoginTdeNavigate,
|
||||
// Navigate to TDE page (if user was on trusted device and TDE has decrypted
|
||||
// their user key, the login-initiated guard will redirect them to the vault)
|
||||
[this.trustedDeviceEncRoute]
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||
await this.router.navigate([this.changePasswordRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async handleForcePasswordReset(orgIdentifier: string) {
|
||||
this.router.navigate([this.forcePasswordResetRoute], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async handleSuccessfulLogin() {
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
// Note: awaiting this will currently cause a hang on desktop & browser as they will wait for a full sync to complete
|
||||
// before navigating to the success route.
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
if (response.resetMasterPassword) {
|
||||
this.successRoute = "set-password";
|
||||
}
|
||||
if (response.forcePasswordReset !== ForceResetPasswordReason.None) {
|
||||
this.successRoute = "update-temp-password";
|
||||
}
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
this.loginService.clearValues();
|
||||
await this.onSuccessfulLoginNavigate();
|
||||
await this.navigateViaCallbackOrRoute(this.onSuccessfulLoginNavigate, [this.successRoute]);
|
||||
}
|
||||
|
||||
private async navigateViaCallbackOrRoute(
|
||||
callback: () => Promise<unknown>,
|
||||
commands: unknown[],
|
||||
extras?: NavigationExtras
|
||||
): Promise<void> {
|
||||
if (callback) {
|
||||
await callback();
|
||||
} else {
|
||||
this.loginService.clearValues();
|
||||
this.router.navigate([this.successRoute], {
|
||||
queryParams: {
|
||||
identifier: this.identifier,
|
||||
},
|
||||
});
|
||||
await this.router.navigate(commands, extras);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
import { DialogServiceAbstraction } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@@ -44,7 +43,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
stateService: StateService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
dialogService: DialogService
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -95,19 +94,19 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: [SymmetricCryptoKey, EncString]
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString]
|
||||
) {
|
||||
try {
|
||||
// Create Request
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
null
|
||||
await this.cryptoService.getOrDeriveMasterKey(this.currentMasterPassword)
|
||||
);
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.key = encKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = newMasterKeyHash;
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
// Update user's password
|
||||
this.apiService.postPassword(request);
|
||||
|
||||
@@ -16,12 +16,11 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import { DialogServiceAbstraction } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@@ -56,7 +55,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private router: Router,
|
||||
dialogService: DialogServiceAbstraction
|
||||
dialogService: DialogService
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -114,21 +113,27 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newKey = await this.cryptoService.makeKey(
|
||||
const newMasterKey = await this.cryptoService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
this.kdf,
|
||||
this.kdfConfig
|
||||
);
|
||||
const newPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, newKey);
|
||||
const newPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey
|
||||
);
|
||||
|
||||
// Grab user's current enc key
|
||||
const userEncKey = await this.cryptoService.getEncKey();
|
||||
// Grab user key
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
|
||||
// Create new encKey for the User
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
|
||||
// Encrypt user key with new master key
|
||||
const newProtectedUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
userKey
|
||||
);
|
||||
|
||||
await this.performSubmitActions(newPasswordHash, newKey, newEncKey);
|
||||
await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
@@ -136,16 +141,16 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: [SymmetricCryptoKey, EncString]
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString]
|
||||
) {
|
||||
try {
|
||||
switch (this.reason) {
|
||||
case ForceResetPasswordReason.AdminForcePasswordReset:
|
||||
this.formPromise = this.updateTempPassword(masterPasswordHash, encKey);
|
||||
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceResetPasswordReason.WeakMasterPassword:
|
||||
this.formPromise = this.updatePassword(masterPasswordHash, encKey);
|
||||
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -167,29 +172,23 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
private async updateTempPassword(
|
||||
masterPasswordHash: string,
|
||||
encKey: [SymmetricCryptoKey, EncString]
|
||||
) {
|
||||
private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = new UpdateTempPasswordRequest();
|
||||
request.key = encKey[1].encryptedString;
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.apiService.putUpdateTempPassword(request);
|
||||
}
|
||||
|
||||
private async updatePassword(
|
||||
newMasterPasswordHash: string,
|
||||
encKey: [SymmetricCryptoKey, EncString]
|
||||
) {
|
||||
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = await this.userVerificationService.buildRequest(
|
||||
this.verification,
|
||||
PasswordRequest
|
||||
);
|
||||
request.masterPasswordHint = this.hint;
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = encKey[1].encryptedString;
|
||||
request.key = userKey[1].encryptedString;
|
||||
|
||||
return this.apiService.postPassword(request);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angu
|
||||
import { ControlValueAccessor, FormControl, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { Verification } from "@bitwarden/common/types/verification";
|
||||
|
||||
/**
|
||||
* Used for general-purpose user verification throughout the app.
|
||||
* Collects the user's master password, or if they are using Key Connector, prompts for an OTP via email.
|
||||
* Collects the user's master password, or if they are not using a password, prompts for an OTP via email.
|
||||
* This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl).
|
||||
* Use UserVerificationService to verify the user's input.
|
||||
*/
|
||||
@@ -40,7 +40,7 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit,
|
||||
}
|
||||
@Output() invalidSecretChange = new EventEmitter<boolean>();
|
||||
|
||||
usesKeyConnector = true;
|
||||
hasMasterPassword = true;
|
||||
disableRequestOTP = false;
|
||||
sentCode = false;
|
||||
|
||||
@@ -50,7 +50,7 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit,
|
||||
if (this.invalidSecret) {
|
||||
return {
|
||||
invalidSecret: {
|
||||
message: this.usesKeyConnector
|
||||
message: this.hasMasterPassword
|
||||
? this.i18nService.t("incorrectCode")
|
||||
: this.i18nService.t("incorrectPassword"),
|
||||
},
|
||||
@@ -63,13 +63,13 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit,
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private cryptoService: CryptoService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
|
||||
this.hasMasterPassword = await this.userVerificationService.hasMasterPasswordAndMasterKeyHash();
|
||||
this.processChanges(this.secret.value);
|
||||
|
||||
this.secret.valueChanges
|
||||
@@ -78,7 +78,7 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit,
|
||||
}
|
||||
|
||||
requestOTP = async () => {
|
||||
if (this.usesKeyConnector) {
|
||||
if (!this.hasMasterPassword) {
|
||||
this.disableRequestOTP = true;
|
||||
try {
|
||||
await this.userVerificationService.requestOTP();
|
||||
@@ -123,7 +123,7 @@ export class UserVerificationComponent implements ControlValueAccessor, OnInit,
|
||||
}
|
||||
|
||||
this.onChange({
|
||||
type: this.usesKeyConnector ? VerificationType.OTP : VerificationType.MasterPassword,
|
||||
type: this.hasMasterPassword ? VerificationType.MasterPassword : VerificationType.OTP,
|
||||
secret: Utils.isNullOrWhitespace(secret) ? null : secret,
|
||||
});
|
||||
}
|
||||
|
||||
5
libs/angular/src/auth/guards/index.ts
Normal file
5
libs/angular/src/auth/guards/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./auth.guard";
|
||||
export * from "./lock.guard";
|
||||
export * from "./redirect.guard";
|
||||
export * from "./tde-decryption-required.guard";
|
||||
export * from "./unauth.guard";
|
||||
@@ -1,25 +1,59 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { CanActivate, Router } from "@angular/router";
|
||||
import { inject } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateFn,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
||||
@Injectable()
|
||||
export class LockGuard implements CanActivate {
|
||||
protected homepage = "vault";
|
||||
protected loginpage = "login";
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
/**
|
||||
* Only allow access to this route if the vault is locked.
|
||||
* If TDE is enabled then the user must also have had a user key at some point.
|
||||
* Otherwise redirect to root.
|
||||
*/
|
||||
export function lockGuard(): CanActivateFn {
|
||||
return async (
|
||||
activatedRouteSnapshot: ActivatedRouteSnapshot,
|
||||
routerStateSnapshot: RouterStateSnapshot
|
||||
) => {
|
||||
const authService = inject(AuthService);
|
||||
const cryptoService = inject(CryptoService);
|
||||
const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction);
|
||||
const router = inject(Router);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
|
||||
async canActivate() {
|
||||
const authStatus = await this.authService.getAuthStatus();
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
if (authStatus !== AuthenticationStatus.Locked) {
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
// User is authN and in locked state.
|
||||
|
||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||
|
||||
// Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow
|
||||
// The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP
|
||||
if (
|
||||
activatedRouteSnapshot.queryParams["from"] === "login-initiated" &&
|
||||
tdeEnabled &&
|
||||
(await userVerificationService.hasMasterPassword())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const redirectUrl =
|
||||
authStatus === AuthenticationStatus.LoggedOut ? this.loginpage : this.homepage;
|
||||
// If authN user with TDE directly navigates to lock, kick them upwards so redirect guard can
|
||||
// properly route them to the login decryption options component.
|
||||
const everHadUserKey = await cryptoService.getEverHadUserKey();
|
||||
if (tdeEnabled && !everHadUserKey) {
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
return this.router.createUrlTree([redirectUrl]);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
58
libs/angular/src/auth/guards/redirect.guard.ts
Normal file
58
libs/angular/src/auth/guards/redirect.guard.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
||||
export interface RedirectRoutes {
|
||||
loggedIn: string;
|
||||
loggedOut: string;
|
||||
locked: string;
|
||||
notDecrypted: string;
|
||||
}
|
||||
|
||||
const defaultRoutes: RedirectRoutes = {
|
||||
loggedIn: "/vault",
|
||||
loggedOut: "/login",
|
||||
locked: "/lock",
|
||||
notDecrypted: "/login-initiated",
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard that consolidates all redirection logic, should be applied to root route.
|
||||
*/
|
||||
export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActivateFn {
|
||||
const routes = { ...defaultRoutes, ...overrides };
|
||||
return async (route) => {
|
||||
const authService = inject(AuthService);
|
||||
const cryptoService = inject(CryptoService);
|
||||
const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction);
|
||||
const router = inject(Router);
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return router.createUrlTree([routes.loggedOut], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||
return router.createUrlTree([routes.loggedIn], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
|
||||
// login decryption options component.
|
||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||
const everHadUserKey = await cryptoService.getEverHadUserKey();
|
||||
if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) {
|
||||
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
return router.createUrlTree([routes.locked], { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
return router.createUrlTree(["/"]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { inject } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
CanActivateFn,
|
||||
} from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
||||
/**
|
||||
* Only allow access to this route if the vault is locked and has never been decrypted.
|
||||
* Otherwise redirect to root.
|
||||
*/
|
||||
export function tdeDecryptionRequiredGuard(): CanActivateFn {
|
||||
return async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const authService = inject(AuthService);
|
||||
const cryptoService = inject(CryptoService);
|
||||
const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction);
|
||||
const router = inject(Router);
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust();
|
||||
const everHadUserKey = await cryptoService.getEverHadUserKey();
|
||||
if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) {
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -19,13 +19,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { CaptchaProtectedComponent } from "../auth/components/captcha-protected.component";
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "../platform/abstractions/form-validation-errors.service";
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../services/dialog";
|
||||
import { PasswordColorText } from "../shared/components/password-strength/password-strength.component";
|
||||
import { InputsFieldMatch } from "../validators/inputsFieldMatch.validator";
|
||||
|
||||
@@ -92,7 +92,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
environmentService: EnvironmentService,
|
||||
protected logService: LogService,
|
||||
protected auditService: AuditService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
@@ -232,7 +232,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -242,7 +242,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -252,7 +252,7 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "exposedMasterPassword" },
|
||||
content: { key: "exposedMasterPasswordDesc" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
@@ -271,16 +271,16 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn
|
||||
const hint = this.formGroup.value.hint;
|
||||
const kdf = DEFAULT_KDF_TYPE;
|
||||
const kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
const key = await this.cryptoService.makeKey(masterPassword, email, kdf, kdfConfig);
|
||||
const encKey = await this.cryptoService.makeEncKey(key);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
|
||||
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
||||
const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
|
||||
const newUserKey = await this.cryptoService.makeUserKey(key);
|
||||
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, key);
|
||||
const keys = await this.cryptoService.makeKeyPair(newUserKey[0]);
|
||||
const request = new RegisterRequest(
|
||||
email,
|
||||
name,
|
||||
hashedPassword,
|
||||
masterKeyHash,
|
||||
hint,
|
||||
encKey[1].encryptedString,
|
||||
newUserKey[1].encryptedString,
|
||||
this.referenceData,
|
||||
this.captchaToken,
|
||||
kdf,
|
||||
|
||||
@@ -18,12 +18,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
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 { MasterKey, UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "../auth/components/change-password.component";
|
||||
import { DialogServiceAbstraction } from "../services/dialog";
|
||||
|
||||
@Directive()
|
||||
export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
@@ -52,7 +52,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
stateService: StateService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
dialogService: DialogServiceAbstraction
|
||||
dialogService: DialogService
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -101,16 +101,16 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: [SymmetricCryptoKey, EncString]
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString]
|
||||
) {
|
||||
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
|
||||
const newKeyPair = await this.cryptoService.makeKeyPair(userKey[0]);
|
||||
const request = new SetPasswordRequest(
|
||||
masterPasswordHash,
|
||||
encKey[1].encryptedString,
|
||||
userKey[1].encryptedString,
|
||||
this.hint,
|
||||
this.identifier,
|
||||
new KeysRequest(keys[0], keys[1].encryptedString),
|
||||
new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString),
|
||||
this.kdf,
|
||||
this.kdfConfig.iterations,
|
||||
this.kdfConfig.memory,
|
||||
@@ -121,7 +121,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
this.formPromise = this.apiService
|
||||
.setPassword(request)
|
||||
.then(async () => {
|
||||
await this.onSetPasswordSuccess(key, encKey, keys);
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
return this.organizationApiService.getKeys(this.orgId);
|
||||
})
|
||||
.then(async (response) => {
|
||||
@@ -131,13 +131,13 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
const userId = await this.stateService.getUserId();
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user's encKey.key with organization public key
|
||||
const userEncKey = await this.cryptoService.getEncKey();
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userEncKey.key, publicKey);
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
const encryptedUserKey = await this.cryptoService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
return this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
@@ -147,7 +147,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.setPassword(request).then(async () => {
|
||||
await this.onSetPasswordSuccess(key, encKey, keys);
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -169,21 +169,21 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||
}
|
||||
|
||||
private async onSetPasswordSuccess(
|
||||
key: SymmetricCryptoKey,
|
||||
encKey: [SymmetricCryptoKey, EncString],
|
||||
keys: [string, EncString]
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
keyPair: [string, EncString]
|
||||
) {
|
||||
await this.stateService.setKdfType(this.kdf);
|
||||
await this.stateService.setKdfConfig(this.kdfConfig);
|
||||
await this.cryptoService.setKey(key);
|
||||
await this.cryptoService.setEncKey(encKey[1].encryptedString);
|
||||
await this.cryptoService.setEncPrivateKey(keys[1].encryptedString);
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey[0]);
|
||||
await this.cryptoService.setPrivateKey(keyPair[1].encryptedString);
|
||||
|
||||
const localKeyHash = await this.cryptoService.hashPassword(
|
||||
const localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
key,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
await this.cryptoService.setKeyHash(localKeyHash);
|
||||
await this.cryptoService.setMasterKeyHash(localMasterKeyHash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { KeySuffixOptions } from "@bitwarden/common/enums/key-suffix-options.enum";
|
||||
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";
|
||||
@@ -17,13 +18,13 @@ export class SetPinComponent implements OnInit {
|
||||
constructor(
|
||||
private modalRef: ModalRef,
|
||||
private cryptoService: CryptoService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showMasterPassOnRestart = this.masterPassOnRestart =
|
||||
!(await this.keyConnectorService.getUsesKeyConnector());
|
||||
await this.userVerificationService.hasMasterPassword();
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
@@ -35,19 +36,22 @@ export class SetPinComponent implements OnInit {
|
||||
this.modalRef.close(false);
|
||||
}
|
||||
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfConfig = await this.stateService.getKdfConfig();
|
||||
const email = await this.stateService.getEmail();
|
||||
const pinKey = await this.cryptoService.makePinKey(this.pin, email, kdf, kdfConfig);
|
||||
const key = await this.cryptoService.getKey();
|
||||
const pinProtectedKey = await this.cryptoService.encrypt(key.key, pinKey);
|
||||
const pinKey = await this.cryptoService.makePinKey(
|
||||
this.pin,
|
||||
await this.stateService.getEmail(),
|
||||
await this.stateService.getKdfType(),
|
||||
await this.stateService.getKdfConfig()
|
||||
);
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey);
|
||||
const encPin = await this.cryptoService.encrypt(this.pin, userKey);
|
||||
await this.stateService.setProtectedPin(encPin.encryptedString);
|
||||
if (this.masterPassOnRestart) {
|
||||
const encPin = await this.cryptoService.encrypt(this.pin);
|
||||
await this.stateService.setProtectedPin(encPin.encryptedString);
|
||||
await this.stateService.setDecryptedPinProtected(pinProtectedKey);
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
|
||||
} else {
|
||||
await this.stateService.setEncryptedPinProtected(pinProtectedKey.encryptedString);
|
||||
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
|
||||
}
|
||||
await this.cryptoService.clearDeprecatedKeys(KeySuffixOptions.Pin);
|
||||
|
||||
this.modalRef.close(true);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
ValidationErrors,
|
||||
Validator,
|
||||
} from "@angular/forms";
|
||||
import { filter, Subject, takeUntil } from "rxjs";
|
||||
import { filter, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Directive()
|
||||
@@ -43,6 +45,8 @@ export class VaultTimeoutInputComponent
|
||||
vaultTimeoutPolicyHours: number;
|
||||
vaultTimeoutPolicyMinutes: number;
|
||||
|
||||
protected canLockVault$: Observable<boolean>;
|
||||
|
||||
private onChange: (vaultTimeout: number) => void;
|
||||
private validatorChange: () => void;
|
||||
private destroy$ = new Subject<void>();
|
||||
@@ -50,6 +54,7 @@ export class VaultTimeoutInputComponent
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
@@ -86,6 +91,10 @@ export class VaultTimeoutInputComponent
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.canLockVault$ = this.vaultTimeoutSettingsService
|
||||
.availableVaultTimeoutActions$()
|
||||
.pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock)));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Dialog, DialogRef } from "@angular/cdk/dialog";
|
||||
|
||||
import { SimpleDialogOptions } from "./simple-dialog-options";
|
||||
|
||||
export abstract class DialogServiceAbstraction extends Dialog {
|
||||
/**
|
||||
* Opens a simple dialog, returns true if the user accepted the dialog.
|
||||
*
|
||||
* @param {SimpleDialogOptions} simpleDialogOptions - An object containing options for the dialog.
|
||||
* @returns `boolean` - True if the user accepted the dialog, false otherwise.
|
||||
*/
|
||||
openSimpleDialog: (simpleDialogOptions: SimpleDialogOptions) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Opens a simple dialog.
|
||||
*
|
||||
* @deprecated Use `openSimpleDialogAcceptedPromise` instead. If you find a use case for the `dialogRef`
|
||||
* please let #wg-component-library know and we can un-deprecate this method.
|
||||
*
|
||||
* @param {SimpleDialogOptions} simpleDialogOptions - An object containing options for the dialog.
|
||||
* @returns `DialogRef` - The reference to the opened dialog.
|
||||
* Contains a closed observable which can be subscribed to for determining which button
|
||||
* a user pressed (see `SimpleDialogCloseType`)
|
||||
*/
|
||||
openSimpleDialogRef: (simpleDialogOptions: SimpleDialogOptions) => DialogRef;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import {
|
||||
DialogRef,
|
||||
DialogConfig,
|
||||
Dialog,
|
||||
DEFAULT_DIALOG_CONFIG,
|
||||
DIALOG_SCROLL_STRATEGY,
|
||||
} from "@angular/cdk/dialog";
|
||||
import { Overlay, OverlayContainer } from "@angular/cdk/overlay";
|
||||
import { ComponentType } from "@angular/cdk/portal";
|
||||
import { Inject, Injectable, Injector, Optional, SkipSelf, TemplateRef } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { DialogServiceAbstraction } from "./dialog.service.abstraction";
|
||||
import { SimpleDialogOptions } from "./simple-dialog-options";
|
||||
import { Translation } from "./translation";
|
||||
|
||||
// This is a temporary base class for Dialogs. It is intended to be removed once the Component Library is adopted by each app.
|
||||
@Injectable()
|
||||
export abstract class DialogService extends Dialog implements DialogServiceAbstraction {
|
||||
constructor(
|
||||
/** Parent class constructor */
|
||||
_overlay: Overlay,
|
||||
_injector: Injector,
|
||||
@Optional() @Inject(DEFAULT_DIALOG_CONFIG) _defaultOptions: DialogConfig,
|
||||
@Optional() @SkipSelf() _parentDialog: Dialog,
|
||||
_overlayContainer: OverlayContainer,
|
||||
@Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
|
||||
protected i18nService: I18nService
|
||||
) {
|
||||
super(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy);
|
||||
}
|
||||
|
||||
async openSimpleDialog(options: SimpleDialogOptions): Promise<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
openSimpleDialogRef(simpleDialogOptions: SimpleDialogOptions): DialogRef {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
override open<R = unknown, D = unknown, C = unknown>(
|
||||
componentOrTemplateRef: ComponentType<C> | TemplateRef<C>,
|
||||
config?: DialogConfig<D, DialogRef<R, C>>
|
||||
): DialogRef<R, C> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
protected translate(translation: string | Translation, defaultKey?: string): string {
|
||||
if (translation == null && defaultKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (translation == null) {
|
||||
return this.i18nService.t(defaultKey);
|
||||
}
|
||||
|
||||
// Translation interface use implies we must localize.
|
||||
if (typeof translation === "object") {
|
||||
return this.i18nService.t(translation.key, ...(translation.placeholders ?? []));
|
||||
}
|
||||
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from "./dialog.service.abstraction";
|
||||
export * from "./simple-dialog-options";
|
||||
export * from "./simple-dialog-type.enum";
|
||||
export * from "./simple-dialog-close-type.enum";
|
||||
export * from "./dialog.service";
|
||||
export * from "./translation";
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum SimpleDialogCloseType {
|
||||
ACCEPT = "accept",
|
||||
CANCEL = "cancel",
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export enum SimpleDialogType {
|
||||
PRIMARY = "primary",
|
||||
SUCCESS = "success",
|
||||
INFO = "info",
|
||||
WARNING = "warning",
|
||||
DANGER = "danger",
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface Translation {
|
||||
key: string;
|
||||
placeholders?: Array<string | number>;
|
||||
}
|
||||
@@ -11,10 +11,12 @@ export const MEMORY_STORAGE = new InjectionToken<AbstractMemoryStorageService>("
|
||||
export const SECURE_STORAGE = new InjectionToken<AbstractStorageService>("SECURE_STORAGE");
|
||||
export const STATE_FACTORY = new InjectionToken<StateFactory>("STATE_FACTORY");
|
||||
export const STATE_SERVICE_USE_CACHE = new InjectionToken<boolean>("STATE_SERVICE_USE_CACHE");
|
||||
export const LOGOUT_CALLBACK = new InjectionToken<(expired: boolean, userId?: string) => void>(
|
||||
"LOGOUT_CALLBACK"
|
||||
export const LOGOUT_CALLBACK = new InjectionToken<
|
||||
(expired: boolean, userId?: string) => Promise<void>
|
||||
>("LOGOUT_CALLBACK");
|
||||
export const LOCKED_CALLBACK = new InjectionToken<(userId?: string) => Promise<void>>(
|
||||
"LOCKED_CALLBACK"
|
||||
);
|
||||
export const LOCKED_CALLBACK = new InjectionToken<() => void>("LOCKED_CALLBACK");
|
||||
export const CLIENT_TYPE = new InjectionToken<boolean>("CLIENT_TYPE");
|
||||
export const LOCALES_DIRECTORY = new InjectionToken<string>("LOCALES_DIRECTORY");
|
||||
export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
|
||||
|
||||
@@ -4,8 +4,7 @@ import { AvatarUpdateService as AccountUpdateServiceAbstraction } from "@bitward
|
||||
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { DeviceCryptoServiceAbstraction } from "@bitwarden/common/abstractions/device-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
|
||||
@@ -18,8 +17,8 @@ import { OrganizationUserService } from "@bitwarden/common/abstractions/organiza
|
||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||
import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
|
||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
InternalOrganizationServiceAbstraction,
|
||||
@@ -41,18 +40,26 @@ import {
|
||||
AccountService as AccountServiceAbstraction,
|
||||
InternalAccountService,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-crypto.service.abstraction";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AuthRequestCryptoServiceImplementation } from "@bitwarden/common/auth/services/auth-request-crypto.service.implementation";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { LoginService } from "@bitwarden/common/auth/services/login.service";
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||
@@ -95,8 +102,7 @@ import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-u
|
||||
import { AnonymousHubService } from "@bitwarden/common/services/anonymousHub.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { DeviceCryptoService } from "@bitwarden/common/services/device-crypto.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/services/devices/devices.service.implementation";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { NotificationsService } from "@bitwarden/common/services/notifications.service";
|
||||
@@ -106,8 +112,8 @@ import { OrganizationUserServiceImplementation } from "@bitwarden/common/service
|
||||
import { SearchService } from "@bitwarden/common/services/search.service";
|
||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||
import { TotpService } from "@bitwarden/common/services/totp.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vaultTimeout/vaultTimeout.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vaultTimeout/vaultTimeoutSettings.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import {
|
||||
PasswordGenerationService,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
@@ -148,7 +154,6 @@ import {
|
||||
} from "@bitwarden/exporter/vault-export";
|
||||
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { LockGuard } from "../auth/guards/lock.guard";
|
||||
import { UnauthGuard } from "../auth/guards/unauth.guard";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { BroadcasterService } from "../platform/services/broadcaster.service";
|
||||
@@ -176,7 +181,6 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
providers: [
|
||||
AuthGuard,
|
||||
UnauthGuard,
|
||||
LockGuard,
|
||||
ModalService,
|
||||
{ provide: WINDOW, useValue: window },
|
||||
{
|
||||
@@ -245,6 +249,8 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
EncryptService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
DeviceTrustCryptoServiceAbstraction,
|
||||
AuthRequestCryptoServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -441,10 +447,11 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
TokenServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
UserVerificationServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutServiceAbstraction,
|
||||
provide: VaultTimeoutService,
|
||||
useClass: VaultTimeoutService,
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
@@ -454,7 +461,6 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
SearchServiceAbstraction,
|
||||
KeyConnectorServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
@@ -462,6 +468,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutServiceAbstraction,
|
||||
useExisting: VaultTimeoutService,
|
||||
},
|
||||
{
|
||||
provide: StateServiceAbstraction,
|
||||
useClass: StateService,
|
||||
@@ -571,6 +581,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
provide: UserVerificationServiceAbstraction,
|
||||
useClass: UserVerificationService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
UserVerificationApiServiceAbstraction,
|
||||
@@ -591,6 +602,17 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
useClass: OrganizationUserServiceImplementation,
|
||||
deps: [ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: PasswordResetEnrollmentServiceAbstraction,
|
||||
useClass: PasswordResetEnrollmentServiceImplementation,
|
||||
deps: [
|
||||
OrganizationApiServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
OrganizationUserService,
|
||||
I18nServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: ProviderServiceAbstraction,
|
||||
useClass: ProviderService,
|
||||
@@ -677,8 +699,13 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
deps: [ApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: DeviceCryptoServiceAbstraction,
|
||||
useClass: DeviceCryptoService,
|
||||
provide: DevicesServiceAbstraction,
|
||||
useClass: DevicesServiceImplementation,
|
||||
deps: [DevicesApiServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: DeviceTrustCryptoServiceAbstraction,
|
||||
useClass: DeviceTrustCryptoService,
|
||||
deps: [
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
@@ -686,8 +713,15 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||
StateServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
DevicesApiServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: AuthRequestCryptoServiceAbstraction,
|
||||
useClass: AuthRequestCryptoServiceImplementation,
|
||||
deps: [CryptoServiceAbstraction],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
|
||||
@@ -21,6 +21,9 @@ export class ModalConfig<D = any> {
|
||||
replaceTopModal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the Component Library's `DialogService` instead.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ModalService {
|
||||
protected modalList: ComponentRef<DynamicModalComponent>[] = [];
|
||||
@@ -50,7 +53,7 @@ export class ModalService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `dialogService.open` (in web) or `modalService.open` (in desktop/browser) instead.
|
||||
* @deprecated Use `dialogService.open` instead.
|
||||
* If replacing an existing call to this method, also remove any `@ViewChild` and `<ng-template>` associated with the
|
||||
* existing usage.
|
||||
*/
|
||||
|
||||
@@ -12,10 +12,9 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
|
||||
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 { DialogService } from "@bitwarden/components";
|
||||
import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../../services/dialog";
|
||||
|
||||
@Directive()
|
||||
export class ExportComponent implements OnInit, OnDestroy {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
@@ -55,7 +54,7 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
private userVerificationService: UserVerificationService,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -135,14 +134,14 @@ export class ExportComponent implements OnInit, OnDestroy {
|
||||
" " +
|
||||
this.i18nService.t("encExportAccountWarningDesc"),
|
||||
acceptButtonText: { key: "exportVault" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
} else {
|
||||
return await this.dialogService.openSimpleDialog({
|
||||
title: { key: "confirmVaultExport" },
|
||||
content: { key: "exportWarningDesc" },
|
||||
acceptButtonText: { key: "exportVault" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ import { SendTextView } from "@bitwarden/common/tools/send/models/view/send-text
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@@ -63,7 +62,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
private logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected sendApiService: SendApiService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("sendTypeFile"), value: SendType.File },
|
||||
@@ -236,7 +235,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteSend" },
|
||||
content: { key: "deleteSendConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -314,7 +313,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
content: { key: this.editMode ? "editedSend" : "createdSend" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
type: SimpleDialogType.SUCCESS,
|
||||
type: "success",
|
||||
});
|
||||
|
||||
await this.copyLinkToClipboard(this.link);
|
||||
|
||||
@@ -12,8 +12,7 @@ import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class SendComponent implements OnInit, OnDestroy {
|
||||
@@ -59,7 +58,7 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
protected policyService: PolicyService,
|
||||
private logService: LogService,
|
||||
protected sendApiService: SendApiService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -139,7 +138,7 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "removePassword" },
|
||||
content: { key: "removePasswordConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -170,7 +169,7 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteSend" },
|
||||
content: { key: "deleteSendConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -33,8 +33,7 @@ import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@@ -101,7 +100,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected passwordRepromptService: PasswordRepromptService,
|
||||
private organizationService: OrganizationService,
|
||||
protected sendApiService: SendApiService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
@@ -406,7 +405,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
content: {
|
||||
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||
},
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -437,16 +436,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "restoreItem" },
|
||||
content: { key: "restoreItemConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.restorePromise = this.restoreCipher();
|
||||
await this.restorePromise;
|
||||
@@ -465,7 +454,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "overwriteUsername" },
|
||||
content: { key: "overwriteUsernameConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -482,7 +471,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "overwritePassword" },
|
||||
content: { key: "overwritePasswordConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -13,8 +13,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class AttachmentsComponent implements OnInit {
|
||||
@@ -43,7 +42,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -106,7 +105,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteAttachment" },
|
||||
content: { key: "deleteAttachmentConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -192,7 +191,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
this.cipherDomain = await this.loadCipher();
|
||||
this.cipher = await this.cipherDomain.decrypt();
|
||||
|
||||
this.hasUpdatedKey = await this.cryptoService.hasEncKey();
|
||||
this.hasUpdatedKey = await this.cryptoService.hasUserKey();
|
||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||
this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null;
|
||||
|
||||
@@ -201,7 +200,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
title: { key: "premiumRequired" },
|
||||
content: { key: "premiumRequiredDesc" },
|
||||
acceptButtonText: { key: "learnMore" },
|
||||
type: SimpleDialogType.SUCCESS,
|
||||
type: "success",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
@@ -212,7 +211,7 @@ export class AttachmentsComponent implements OnInit {
|
||||
title: { key: "featureUnavailable" },
|
||||
content: { key: "updateKey" },
|
||||
acceptButtonText: { key: "learnMore" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
|
||||
@@ -7,8 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class FolderAddEditComponent implements OnInit {
|
||||
@@ -33,7 +32,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected dialogService: DialogServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
@@ -74,7 +73,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "deleteFolder" },
|
||||
content: { key: "deleteFolderConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class PremiumComponent implements OnInit {
|
||||
@@ -22,7 +21,7 @@ export class PremiumComponent implements OnInit {
|
||||
protected apiService: ApiService,
|
||||
private logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected dialogService: DialogServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
private environmentService: EnvironmentService
|
||||
) {
|
||||
this.cloudWebVaultUrl = this.environmentService.getCloudWebVaultUrl();
|
||||
@@ -47,7 +46,7 @@ export class PremiumComponent implements OnInit {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "premiumPurchase" },
|
||||
content: { key: "premiumPurchaseAlert" },
|
||||
type: SimpleDialogType.INFO,
|
||||
type: "info",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
@@ -59,7 +58,7 @@ export class PremiumComponent implements OnInit {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "premiumManage" },
|
||||
content: { key: "premiumManageAlert" },
|
||||
type: SimpleDialogType.INFO,
|
||||
type: "info",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
|
||||
@@ -34,8 +34,7 @@ import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { DialogServiceAbstraction, SimpleDialogType } from "../../services/dialog";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
const BroadcasterSubscriptionId = "ViewComponent";
|
||||
|
||||
@@ -88,7 +87,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
private logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected fileDownloadService: FileDownloadService,
|
||||
protected dialogService: DialogServiceAbstraction
|
||||
protected dialogService: DialogService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -160,7 +159,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "passkeyNotCopied" },
|
||||
content: { key: "passkeyNotCopiedAlert" },
|
||||
type: SimpleDialogType.INFO,
|
||||
type: "info",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -195,7 +194,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
content: {
|
||||
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
||||
},
|
||||
type: SimpleDialogType.WARNING,
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
@@ -222,16 +221,6 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "restoreItem" },
|
||||
content: { key: "restoreItemConfirmation" },
|
||||
type: SimpleDialogType.WARNING,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.restoreCipher();
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
|
||||
import { ModalService } from "../../services/modal.service";
|
||||
|
||||
import { PasswordRepromptService } from "./password-reprompt.service";
|
||||
|
||||
describe("PasswordRepromptService", () => {
|
||||
let passwordRepromptService: PasswordRepromptService;
|
||||
|
||||
let userVerificationService: MockProxy<UserVerificationService>;
|
||||
let modalService: MockProxy<ModalService>;
|
||||
|
||||
beforeEach(() => {
|
||||
modalService = mock<ModalService>();
|
||||
userVerificationService = mock<UserVerificationService>();
|
||||
|
||||
passwordRepromptService = new PasswordRepromptService(modalService, userVerificationService);
|
||||
});
|
||||
|
||||
describe("enabled()", () => {
|
||||
it("returns false if a user does not have a master password", async () => {
|
||||
userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(false);
|
||||
|
||||
expect(await passwordRepromptService.enabled()).toBe(false);
|
||||
});
|
||||
it("returns true if the user has a master password", async () => {
|
||||
userVerificationService.hasMasterPasswordAndMasterKeyHash.mockResolvedValue(true);
|
||||
|
||||
expect(await passwordRepromptService.enabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/vault/abstractions/password-reprompt.service";
|
||||
|
||||
import { ModalService } from "../../services/modal.service";
|
||||
@@ -16,7 +16,7 @@ export class PasswordRepromptService implements PasswordRepromptServiceAbstracti
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private keyConnectorService: KeyConnectorService
|
||||
private userVerificationService: UserVerificationService
|
||||
) {}
|
||||
|
||||
protectedFields() {
|
||||
@@ -39,6 +39,6 @@ export class PasswordRepromptService implements PasswordRepromptServiceAbstracti
|
||||
}
|
||||
|
||||
async enabled() {
|
||||
return !(await this.keyConnectorService.getUsesKeyConnector());
|
||||
return await this.userVerificationService.hasMasterPasswordAndMasterKeyHash();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ export abstract class ApiService {
|
||||
postConvertToKeyConnector: () => Promise<void>;
|
||||
//passwordless
|
||||
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||
@@ -523,7 +524,7 @@ export abstract class ApiService {
|
||||
) => Promise<void>;
|
||||
postResendSponsorshipOffer: (sponsoringOrgId: string) => Promise<void>;
|
||||
|
||||
getUserKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
|
||||
getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
|
||||
postUserKeyToKeyConnector: (
|
||||
keyConnectorUrl: string,
|
||||
request: KeyConnectorUserKeyRequest
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DeviceKey } from "../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { DeviceResponse } from "./devices/responses/device.response";
|
||||
|
||||
export abstract class DeviceCryptoServiceAbstraction {
|
||||
trustDevice: () => Promise<DeviceResponse>;
|
||||
getDeviceKey: () => Promise<DeviceKey>;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { DeviceResponse } from "./responses/device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserSymKey: string,
|
||||
userSymKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { DeviceView } from "./views/device.view";
|
||||
|
||||
export abstract class DevicesServiceAbstraction {
|
||||
getDevices$: () => Observable<Array<DeviceView>>;
|
||||
getDeviceByIdentifier$: (deviceIdentifier: string) => Observable<DeviceView>;
|
||||
isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable<boolean>;
|
||||
updateTrustedDeviceKeys$: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserKey: string,
|
||||
userKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Observable<DeviceView>;
|
||||
}
|
||||
@@ -3,23 +3,20 @@ import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class DeviceResponse extends BaseResponse {
|
||||
id: string;
|
||||
name: number;
|
||||
userId: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
type: DeviceType;
|
||||
creationDate: string;
|
||||
encryptedUserKey: string;
|
||||
encryptedPublicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
|
||||
revisionDate: string;
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.userId = this.getResponseProperty("UserId");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.identifier = this.getResponseProperty("Identifier");
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
|
||||
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
|
||||
this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
}
|
||||
}
|
||||
|
||||
17
libs/common/src/abstractions/devices/views/device.view.ts
Normal file
17
libs/common/src/abstractions/devices/views/device.view.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DeviceType } from "../../../enums";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { DeviceResponse } from "../responses/device.response";
|
||||
|
||||
export class DeviceView implements View {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
identifier: string;
|
||||
type: DeviceType;
|
||||
creationDate: string;
|
||||
revisionDate: string;
|
||||
|
||||
constructor(deviceResponse: DeviceResponse) {
|
||||
Object.assign(this, deviceResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
|
||||
|
||||
export abstract class VaultTimeoutSettingsService {
|
||||
/**
|
||||
* Set the vault timeout options for the user
|
||||
* @param vaultTimeout The vault timeout in minutes
|
||||
* @param vaultTimeoutAction The vault timeout action
|
||||
* @param userId The user id to set. If not provided, the current user is used
|
||||
*/
|
||||
setVaultTimeoutOptions: (
|
||||
vaultTimeout: number,
|
||||
vaultTimeoutAction: VaultTimeoutAction
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the available vault timeout actions for the current user
|
||||
*
|
||||
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
*/
|
||||
availableVaultTimeoutActions$: (userId?: string) => Observable<VaultTimeoutAction[]>;
|
||||
|
||||
/**
|
||||
* Get the current vault timeout action for the user. This is not the same as the current state, it is
|
||||
* calculated based on the current state, the user's policy, and the user's available unlock methods.
|
||||
*/
|
||||
getVaultTimeout: (userId?: string) => Promise<number>;
|
||||
|
||||
/**
|
||||
* Observe the vault timeout action for the user. This is calculated based on users preferred lock action saved in the state,
|
||||
* the user's policy, and the user's available unlock methods.
|
||||
*
|
||||
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
*/
|
||||
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
|
||||
|
||||
/**
|
||||
* Has the user enabled unlock with Pin.
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
* @returns PinLockType
|
||||
*/
|
||||
isPinLockSet: (userId?: string) => Promise<PinLockType>;
|
||||
|
||||
/**
|
||||
* Has the user enabled unlock with Biometric.
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
* @returns boolean true if biometric lock is set
|
||||
*/
|
||||
isBiometricLockSet: (userId?: string) => Promise<boolean>;
|
||||
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
|
||||
export abstract class VaultTimeoutSettingsService {
|
||||
setVaultTimeoutOptions: (
|
||||
vaultTimeout: number,
|
||||
vaultTimeoutAction: VaultTimeoutAction
|
||||
) => Promise<void>;
|
||||
getVaultTimeout: (userId?: string) => Promise<number>;
|
||||
getVaultTimeoutAction: (userId?: string) => Promise<VaultTimeoutAction>;
|
||||
isPinLockSet: () => Promise<[boolean, boolean]>;
|
||||
isBiometricLockSet: () => Promise<boolean>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { UserKey, MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthRequestResponse } from "../models/response/auth-request.response";
|
||||
|
||||
export abstract class AuthRequestCryptoServiceAbstraction {
|
||||
setUserKeyAfterDecryptingSharedUserKey: (
|
||||
authReqResponse: AuthRequestResponse,
|
||||
authReqPrivateKey: ArrayBuffer
|
||||
) => Promise<void>;
|
||||
setKeysAfterDecryptingSharedMasterKeyAndHash: (
|
||||
authReqResponse: AuthRequestResponse,
|
||||
authReqPrivateKey: ArrayBuffer
|
||||
) => Promise<void>;
|
||||
|
||||
decryptPubKeyEncryptedUserKey: (
|
||||
pubKeyEncryptedUserKey: string,
|
||||
privateKey: ArrayBuffer
|
||||
) => Promise<UserKey>;
|
||||
|
||||
decryptPubKeyEncryptedMasterKeyAndHash: (
|
||||
pubKeyEncryptedMasterKey: string,
|
||||
pubKeyEncryptedMasterKeyHash: string,
|
||||
privateKey: ArrayBuffer
|
||||
) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { AuthRequestPushNotification } from "../../models/response/notification.response";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||
import { AuthResult } from "../models/domain/auth-result";
|
||||
import {
|
||||
@@ -32,7 +32,7 @@ export abstract class AuthService {
|
||||
captchaResponse: string
|
||||
) => Promise<AuthResult>;
|
||||
logOut: (callback: () => void) => void;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||
authingWithUserApiKey: () => boolean;
|
||||
authingWithSso: () => boolean;
|
||||
authingWithPassword: () => boolean;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { DeviceKey, UserKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class DeviceTrustCryptoServiceAbstraction {
|
||||
/**
|
||||
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
||||
* Note: this value should only be used once and then reset
|
||||
*/
|
||||
getShouldTrustDevice: () => Promise<boolean | null>;
|
||||
setShouldTrustDevice: (value: boolean) => Promise<void>;
|
||||
|
||||
trustDeviceIfRequired: () => Promise<void>;
|
||||
|
||||
trustDevice: () => Promise<DeviceResponse>;
|
||||
getDeviceKey: () => Promise<DeviceKey>;
|
||||
decryptUserKeyWithDeviceKey: (
|
||||
encryptedDevicePrivateKey: EncString,
|
||||
encryptedUserKey: EncString,
|
||||
deviceKey?: DeviceKey
|
||||
) => Promise<UserKey | null>;
|
||||
rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise<void>;
|
||||
|
||||
supportsDeviceTrust: () => Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
|
||||
export abstract class DevicesApiServiceAbstraction {
|
||||
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
|
||||
|
||||
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
|
||||
|
||||
getDevices: () => Promise<ListResponse<DeviceResponse>>;
|
||||
|
||||
updateTrustedDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserKey: string,
|
||||
userKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
) => Promise<DeviceResponse>;
|
||||
|
||||
updateTrust: (updateDevicesTrustRequestModel: UpdateDevicesTrustRequest) => Promise<void>;
|
||||
|
||||
getDeviceKeys: (
|
||||
deviceIdentifier: string,
|
||||
secretVerificationRequest: SecretVerificationRequest
|
||||
) => Promise<ProtectedDeviceResponse>;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
getAndSetKey: (url?: string) => Promise<void>;
|
||||
setMasterKeyFromUrl: (url?: string) => Promise<void>;
|
||||
getManagingOrganization: () => Promise<Organization>;
|
||||
getUsesKeyConnector: () => Promise<boolean>;
|
||||
migrateUser: () => Promise<void>;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class PasswordResetEnrollmentServiceAbstraction {
|
||||
/*
|
||||
* Checks the user's enrollment status and enrolls them if required
|
||||
*/
|
||||
abstract enrollIfRequired(organizationSsoIdentifier: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enroll current user in password reset
|
||||
* @param organizationId - Organization in which to enroll the user
|
||||
* @returns Promise that resolves when the user is enrolled
|
||||
* @throws Error if the action fails
|
||||
*/
|
||||
abstract enroll(organizationId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Enroll user in password reset
|
||||
* @param organizationId - Organization in which to enroll the user
|
||||
* @param userId - User to enroll
|
||||
* @param userKey - User's symmetric key
|
||||
* @returns Promise that resolves when the user is enrolled
|
||||
* @throws Error if the action fails
|
||||
*/
|
||||
abstract enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
|
||||
}
|
||||
@@ -9,4 +9,16 @@ export abstract class UserVerificationService {
|
||||
) => Promise<T>;
|
||||
verifyUser: (verification: Verification) => Promise<boolean>;
|
||||
requestOTP: () => Promise<void>;
|
||||
/**
|
||||
* Check if user has master password or only uses passwordless technologies to log in
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
* @returns True if the user has a master password
|
||||
*/
|
||||
hasMasterPassword: (userId?: string) => Promise<boolean>;
|
||||
/**
|
||||
* Check if the user has a master password and has used it during their current session
|
||||
* @param userId The user id to check. If not provided, the current user id used
|
||||
* @returns True if the user has a master password and has used it in the current session
|
||||
*/
|
||||
hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AuthRequestType {
|
||||
AuthenticateAndUnlock = 0,
|
||||
Unlock = 1,
|
||||
AdminApproval = 2,
|
||||
}
|
||||
|
||||
@@ -9,12 +9,25 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
|
||||
import {
|
||||
Account,
|
||||
AccountDecryptionOptions,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../../platform/models/domain/account";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import {
|
||||
DeviceKey,
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "../../tools/password-strength";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -28,6 +41,10 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
||||
import {
|
||||
IUserDecryptionOptionsServerResponse,
|
||||
UserDecryptionOptionsResponse,
|
||||
} from "../models/response/user-decryption-options/user-decryption-options.response";
|
||||
|
||||
import { PasswordLogInStrategy } from "./password-login.strategy";
|
||||
|
||||
@@ -37,7 +54,7 @@ const masterPassword = "password";
|
||||
const deviceId = Utils.newGuid();
|
||||
const accessToken = "ACCESS_TOKEN";
|
||||
const refreshToken = "REFRESH_TOKEN";
|
||||
const encKey = "ENC_KEY";
|
||||
const userKey = "USER_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const captchaSiteKey = "CAPTCHA_SITE_KEY";
|
||||
const kdf = 0;
|
||||
@@ -45,6 +62,13 @@ const kdfIterations = 10000;
|
||||
const userId = Utils.newGuid();
|
||||
const masterPasswordHash = "MASTER_PASSWORD_HASH";
|
||||
const name = "NAME";
|
||||
const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: true,
|
||||
};
|
||||
const userDecryptionOptions = new UserDecryptionOptionsResponse(
|
||||
defaultUserDecryptionOptionsServerResponse
|
||||
);
|
||||
const acctDecryptionOptions = AccountDecryptionOptions.fromResponse(userDecryptionOptions);
|
||||
|
||||
const decodedToken = {
|
||||
sub: userId,
|
||||
@@ -58,13 +82,14 @@ const twoFactorToken = "TWO_FACTOR_TOKEN";
|
||||
const twoFactorRemember = true;
|
||||
|
||||
export function identityTokenResponseFactory(
|
||||
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null
|
||||
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null,
|
||||
userDecryptionOptions: IUserDecryptionOptionsServerResponse = null
|
||||
) {
|
||||
return new IdentityTokenResponse({
|
||||
ForcePasswordReset: false,
|
||||
Kdf: kdf,
|
||||
KdfIterations: kdfIterations,
|
||||
Key: encKey,
|
||||
Key: userKey,
|
||||
PrivateKey: privateKey,
|
||||
ResetMasterPassword: false,
|
||||
access_token: accessToken,
|
||||
@@ -73,9 +98,11 @@ export function identityTokenResponseFactory(
|
||||
scope: "api offline_access",
|
||||
token_type: "Bearer",
|
||||
MasterPasswordPolicy: masterPasswordPolicyResponse,
|
||||
UserDecryptionOptions: userDecryptionOptions || defaultUserDecryptionOptionsServerResponse,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: add tests for latest changes to base class for TDE
|
||||
describe("LogInStrategy", () => {
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
@@ -129,8 +156,23 @@ describe("LogInStrategy", () => {
|
||||
});
|
||||
|
||||
describe("base class", () => {
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
const userKeyBytesLength = 64;
|
||||
const masterKeyBytesLength = 64;
|
||||
let userKey: UserKey;
|
||||
let masterKey: MasterKey;
|
||||
|
||||
beforeEach(() => {
|
||||
userKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(userKeyBytesLength).buffer as CsprngArray
|
||||
) as UserKey;
|
||||
masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(masterKeyBytesLength).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login with master password", async () => {
|
||||
const idTokenResponse = identityTokenResponseFactory();
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
@@ -154,13 +196,36 @@ describe("LogInStrategy", () => {
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
},
|
||||
keys: new AccountKeys(),
|
||||
decryptionOptions: acctDecryptionOptions,
|
||||
})
|
||||
);
|
||||
expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey);
|
||||
expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||
});
|
||||
|
||||
it("persists a device key for trusted device encryption when it exists on login", async () => {
|
||||
// Arrange
|
||||
const idTokenResponse = identityTokenResponseFactory();
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
|
||||
const deviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(userKeyBytesLength).buffer as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
stateService.getDeviceKey.mockResolvedValue(deviceKey);
|
||||
|
||||
const accountKeys = new AccountKeys();
|
||||
accountKeys.deviceKey = deviceKey;
|
||||
|
||||
// Act
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(stateService.addAccount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ keys: accountKeys })
|
||||
);
|
||||
});
|
||||
|
||||
it("builds AuthResult", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.forcePasswordReset = true;
|
||||
@@ -187,6 +252,8 @@ describe("LogInStrategy", () => {
|
||||
});
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
@@ -204,13 +271,15 @@ describe("LogInStrategy", () => {
|
||||
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey
|
||||
expect(cryptoService.setKey).toHaveBeenCalled();
|
||||
// User symmetric key must be set before the new RSA keypair is generated
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalled();
|
||||
expect(cryptoService.makeKeyPair).toHaveBeenCalled();
|
||||
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
expect(cryptoService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
cryptoService.makeKeyPair.mock.invocationCallOrder[0]
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Account, AccountProfile, AccountTokens } from "../../platform/models/domain/account";
|
||||
import {
|
||||
Account,
|
||||
AccountDecryptionOptions,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../../platform/models/domain/account";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
|
||||
@@ -53,9 +59,6 @@ export abstract class LogInStrategy {
|
||||
| PasswordlessLogInCredentials
|
||||
): Promise<AuthResult>;
|
||||
|
||||
// The user key comes from different sources depending on the login strategy
|
||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string = null
|
||||
@@ -101,12 +104,28 @@ export abstract class LogInStrategy {
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
||||
|
||||
// Must persist existing device key if it exists for trusted device decryption to work
|
||||
// However, we must provide a user id so that the device key can be retrieved
|
||||
// as the state service won't have an active account at this point in time
|
||||
// even though the data exists in local storage.
|
||||
const userId = accountInformation.sub;
|
||||
|
||||
const deviceKey = await this.stateService.getDeviceKey({ userId });
|
||||
const accountKeys = new AccountKeys();
|
||||
if (deviceKey) {
|
||||
accountKeys.deviceKey = deviceKey;
|
||||
}
|
||||
|
||||
// If you don't persist existing admin auth requests on login, they will get deleted.
|
||||
const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId });
|
||||
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
userId,
|
||||
name: accountInformation.name,
|
||||
email: accountInformation.email,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
@@ -123,6 +142,11 @@ export abstract class LogInStrategy {
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
keys: accountKeys,
|
||||
decryptionOptions: AccountDecryptionOptions.fromResponse(
|
||||
tokenResponse.userDecryptionOptions
|
||||
),
|
||||
adminAuthRequest: adminAuthRequest?.toJSON(),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -135,28 +159,41 @@ export abstract class LogInStrategy {
|
||||
result.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
|
||||
}
|
||||
|
||||
// Must come before setting keys, user key needs email to update additional keys
|
||||
await this.saveAccountInformation(response);
|
||||
|
||||
if (response.twoFactorToken != null) {
|
||||
await this.tokenService.setTwoFactorToken(response);
|
||||
}
|
||||
|
||||
await this.setMasterKey(response);
|
||||
|
||||
await this.setUserKey(response);
|
||||
|
||||
// Must come after the user Key is set, otherwise createKeyPairForOldAccount will fail
|
||||
const newSsoUser = response.key == null;
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setEncKey(response.key);
|
||||
await this.cryptoService.setEncPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
await this.setPrivateKey(response);
|
||||
|
||||
this.messagingService.send("loggedIn");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// The keys comes from different sources depending on the login strategy
|
||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
protected async createKeyPairForOldAccount() {
|
||||
try {
|
||||
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
|
||||
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
|
||||
return privateKey.encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.twoFactorProviders = response.twoFactorProviders2;
|
||||
@@ -173,14 +210,4 @@ export abstract class LogInStrategy {
|
||||
result.captchaSiteKey = response.siteKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
private async createKeyPairForOldAccount() {
|
||||
try {
|
||||
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
|
||||
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
|
||||
return privateKey.encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,23 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
PasswordStrengthService,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
} from "../../tools/password-strength";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
|
||||
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
|
||||
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
|
||||
|
||||
@@ -31,11 +37,11 @@ const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
const hashedPassword = "HASHED_PASSWORD";
|
||||
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
|
||||
const preloginKey = new SymmetricCryptoKey(
|
||||
const masterKey = new SymmetricCryptoKey(
|
||||
Utils.fromB64ToArray(
|
||||
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
|
||||
)
|
||||
);
|
||||
) as MasterKey;
|
||||
const deviceId = Utils.newGuid();
|
||||
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
|
||||
EnforceOnLogin: true,
|
||||
@@ -58,6 +64,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
@@ -76,12 +83,12 @@ describe("PasswordLogInStrategy", () => {
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
authService.makePreloginKey.mockResolvedValue(preloginKey);
|
||||
authService.makePreloginKey.mockResolvedValue(masterKey);
|
||||
|
||||
cryptoService.hashPassword
|
||||
cryptoService.hashMasterKey
|
||||
.calledWith(masterPassword, expect.anything(), undefined)
|
||||
.mockResolvedValue(hashedPassword);
|
||||
cryptoService.hashPassword
|
||||
cryptoService.hashMasterKey
|
||||
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
|
||||
.mockResolvedValue(localHashedPassword);
|
||||
|
||||
@@ -102,10 +109,9 @@ describe("PasswordLogInStrategy", () => {
|
||||
authService
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(
|
||||
identityTokenResponseFactory(masterPasswordPolicy)
|
||||
);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
});
|
||||
|
||||
it("sends master password credentials to the server", async () => {
|
||||
@@ -127,15 +133,23 @@ describe("PasswordLogInStrategy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
it("sets keys after a successful authentication", async () => {
|
||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
|
||||
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
|
||||
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when there are no requirements", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory(null));
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory());
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
@@ -35,8 +35,8 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
|
||||
private localHashedPassword: string;
|
||||
private key: SymmetricCryptoKey;
|
||||
private localMasterKeyHash: string;
|
||||
private masterKey: MasterKey;
|
||||
|
||||
/**
|
||||
* Options to track if the user needs to update their password due to a password that does not meet an organization's
|
||||
@@ -71,12 +71,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey() {
|
||||
await this.cryptoService.setKey(this.key);
|
||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
@@ -96,28 +91,29 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
return result;
|
||||
}
|
||||
|
||||
async logIn(credentials: PasswordLogInCredentials) {
|
||||
override async logIn(credentials: PasswordLogInCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
|
||||
this.key = await this.authService.makePreloginKey(masterPassword, email);
|
||||
this.masterKey = await this.authService.makePreloginKey(masterPassword, email);
|
||||
|
||||
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
||||
this.localHashedPassword = await this.cryptoService.hashPassword(
|
||||
this.localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||
masterPassword,
|
||||
this.key,
|
||||
this.masterKey,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
|
||||
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, this.masterKey);
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
hashedPassword,
|
||||
masterKeyHash,
|
||||
captchaToken,
|
||||
await this.buildTwoFactor(twoFactor),
|
||||
await this.buildDeviceRequest()
|
||||
);
|
||||
|
||||
const [authResult, identityResponse] = await this.startLogIn();
|
||||
|
||||
const masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
@@ -145,6 +141,27 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
await this.cryptoService.setMasterKey(this.masterKey);
|
||||
await this.cryptoService.setMasterKeyHash(this.localMasterKeyHash);
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
|
||||
private getMasterPasswordPolicyOptionsFromResponse(
|
||||
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
|
||||
): MasterPasswordPolicyOptions {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { PasswordlessLogInStrategy } from "./passwordless-login.strategy";
|
||||
|
||||
describe("PasswordlessLogInStrategy", () => {
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
|
||||
let passwordlessLoginStrategy: PasswordlessLogInStrategy;
|
||||
let credentials: PasswordlessLogInCredentials;
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
|
||||
const email = "EMAIL";
|
||||
const accessCode = "ACCESS_CODE";
|
||||
const authRequestId = "AUTH_REQUEST_ID";
|
||||
const decMasterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
const decUserKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
const decMasterKeyHash = "LOCAL_PASSWORD_HASH";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
passwordlessLoginStrategy = new PasswordlessLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
deviceTrustCryptoService
|
||||
);
|
||||
|
||||
tokenResponse = identityTokenResponseFactory();
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
});
|
||||
|
||||
it("sets keys after a successful authentication when masterKey and masterKeyHash provided in login credentials", async () => {
|
||||
credentials = new PasswordlessLogInCredentials(
|
||||
email,
|
||||
accessCode,
|
||||
authRequestId,
|
||||
null,
|
||||
decMasterKey,
|
||||
decMasterKeyHash
|
||||
);
|
||||
|
||||
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await passwordlessLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
|
||||
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
|
||||
// Initialize credentials with only userKey
|
||||
credentials = new PasswordlessLogInCredentials(
|
||||
email,
|
||||
accessCode,
|
||||
authRequestId,
|
||||
decUserKey, // Pass userKey
|
||||
null, // No masterKey
|
||||
null // No masterKeyHash
|
||||
);
|
||||
|
||||
// Call logIn
|
||||
await passwordlessLoginStrategy.logIn(credentials);
|
||||
|
||||
// setMasterKey and setMasterKeyHash should not be called
|
||||
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled();
|
||||
|
||||
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
|
||||
// trustDeviceIfRequired should be called
|
||||
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,14 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { AuthService } from "../abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { AuthResult } from "../models/domain/auth-result";
|
||||
import { PasswordlessLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
import { LogInStrategy } from "./login.strategy";
|
||||
|
||||
@@ -41,7 +42,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private authService: AuthService
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
@@ -56,20 +57,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey() {
|
||||
await this.cryptoService.setKey(this.passwordlessCredentials.decKey);
|
||||
await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash);
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
async logIn(credentials: PasswordlessLogInCredentials) {
|
||||
override async logIn(credentials: PasswordlessLogInCredentials) {
|
||||
this.passwordlessCredentials = credentials;
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
@@ -84,4 +72,52 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
|
||||
override async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string
|
||||
): Promise<AuthResult> {
|
||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
||||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
if (
|
||||
this.passwordlessCredentials.decryptedMasterKey &&
|
||||
this.passwordlessCredentials.decryptedMasterKeyHash
|
||||
) {
|
||||
await this.cryptoService.setMasterKey(this.passwordlessCredentials.decryptedMasterKey);
|
||||
await this.cryptoService.setMasterKeyHash(
|
||||
this.passwordlessCredentials.decryptedMasterKeyHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
// User now may or may not have a master password
|
||||
// but set the master key encrypted user key if it exists regardless
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
|
||||
if (this.passwordlessCredentials.decryptedUserKey) {
|
||||
await this.cryptoService.setUserKey(this.passwordlessCredentials.decryptedUserKey);
|
||||
} else {
|
||||
await this.trySetUserKeyWithMasterKey();
|
||||
// Establish trust if required after setting user key
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
}
|
||||
}
|
||||
|
||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,34 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
DeviceKey,
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
import { SsoLogInCredentials } from "../models/domain/log-in-credentials";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "../models/response/user-decryption-options/user-decryption-options.response";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { SsoLogInStrategy } from "./sso-login.strategy";
|
||||
|
||||
// TODO: Add tests for new trySetUserKeyWithApprovedAdminRequestIfExists logic
|
||||
// https://bitwarden.atlassian.net/browse/PM-3339
|
||||
|
||||
describe("SsoLogInStrategy", () => {
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
@@ -27,6 +42,9 @@ describe("SsoLogInStrategy", () => {
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||
let authRequestCryptoService: MockProxy<AuthRequestCryptoServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
|
||||
let ssoLogInStrategy: SsoLogInStrategy;
|
||||
let credentials: SsoLogInCredentials;
|
||||
@@ -50,6 +68,9 @@ describe("SsoLogInStrategy", () => {
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||
authRequestCryptoService = mock<AuthRequestCryptoServiceAbstraction>();
|
||||
i18nService = mock<I18nService>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
@@ -65,7 +86,10 @@ describe("SsoLogInStrategy", () => {
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
keyConnectorService
|
||||
keyConnectorService,
|
||||
deviceTrustCryptoService,
|
||||
authRequestCryptoService,
|
||||
i18nService
|
||||
);
|
||||
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
||||
});
|
||||
@@ -98,33 +122,194 @@ describe("SsoLogInStrategy", () => {
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setEncKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setMasterKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setPrivateKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets and sets KeyConnector key for enrolled user", async () => {
|
||||
it("sets master key encrypted user key for existing SSO users", async () => {
|
||||
// Arrange
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
// Act
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
// Assert
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
});
|
||||
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
tokenResponse.key = null;
|
||||
describe("Trusted Device Decryption", () => {
|
||||
const deviceKeyBytesLength = 64;
|
||||
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
|
||||
const mockDeviceKey: DeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
const userKeyBytesLength = 64;
|
||||
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength).buffer as CsprngArray;
|
||||
const mockUserKey: UserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
const mockEncDevicePrivateKey =
|
||||
"2.eh465OrUcluL9UpnCOUTAg==|2HXNXwrLwAjUfZ/U75c92rZEltt1eHxjMkp/ADAmx346oT1+GaQvaL1QIV/9Om0T72m8AnlO92iUfWdhbA/ifHZ+lhFoUVeyw1M88CMzktbVcq42rFoK7SGHSAGdTL3ccUWKI8yCCQJhpt2X6a/5+T7ey5k2CqvylKyOtkiCnVeLmYqETn5BM9Rl3tEgJW1yDLuSJ+L+Qh9xnk/Z3zJUV5HAs+YwjKwuSNrd00SXjDyx8rBEstD9MKI+lrk7to/q90vqKqCucAj/dzUpVtHe88al2AAlBVwQ13HUPdNFOyti6niUgCAWx+DzRqlhkFvl/z/rtxtQsyqq/3Eh/EL54ylxKzAya0ev9EaIOm/dD1aBmI58p4Bs0eMOCIKJjtw+Cmdql+RhCtKtumgFShqyXv+LfD/FgUsdTVNExk3YNhgwPR4jOaMa/j9LCrBMCLKxdAhQyBe7T3qoX1fBBirvY6t77ifMu1YEQ6DfmFphVSwDH5C9xGeTSh5IELSf0tGVtlWUe9RffDDzccD0L1lR8U+dqzoSTYCuXvhEhQptdIW6fpH/47u0M5MiI97/d35A7Et2I1gjHp7WF3qsY20ellBueu7ZL5P1BmqPXl58yaBBXJaCutYHDfIucspqdZmfBGEbdRT4wmuZRON0J8zLmUejM0VR/2MOmpfyYQXnJhTfrvnZ1bOg1aMhUxJ2vhDNPXUFm5b+vwsho4GEvcLAKq9WwbvOJ/sK7sEVfTfEO2IG+0X6wkWm7RpR6Wq9FGKSrv2PSjMAYnb+z3ETeWiaaiD+tVFxa2AaqsbOuX092/86GySpHES7cFWhQ/YMOgj6egUi8mEC0CqMXYsx0TTJDsn16oP+XB3a2WoRqzE0YBozp2aMXxhVf/jMZ03BmEmRQu5B+Sq1gMEZwtIfJ+srkZLMYlLjvVw92FRoFy+N6ytPiyf6RMHMUnJ3vEZSBogaElYoQAtFJ5kK811CUzb78zEHH8xWtPrCZn9zZfvf/zaWxo7fpV8VwAwUeHXHcQMraZum5QeO+5tLRUYrLm85JNelGfmUA3BjfNyFbfb32PhkWWd0CbDaPME48uIriVK32pNEtvtR/+I/f3YgA/jP9kSlDvbzG/OAg/AFBIpNwKUzsu4+va8mI+O5FDufw5D74WwdGJ9DeyEb2CHtWMR1VwtFKL0ZZsqltNf8EkBeJ5RtTNtAMM8ie4dDZaKC96ymQHKrdB4hjkAr0F1XFsU4XdOa9Nbkdcm/7KoNc6bE6oJtG9lqE8h+1CysfcbfJ7am+hvDFzT0IPmp3GDSMAk+e6xySgFQw0C/SZ7LQsxPa1s6hc+BOtTn0oClZnU7Mowxv+z+xURJj4Yp3Cy6tAoia1jEQSs6lSMNKPf9bi3xFKtPl4143hwhpvTAzJUcski9OVGd7Du+VyxwIrvLqp5Ct/oNrESVJpf1EDCs9xT1EW+PiSkRmHXoZ1t5MOLFEiMAZL2+bNe3A2661oJeMtps8zrfCVc251OUE1WvqWePlTOs5TDVqdwDH88J6rHLsbaf33Mxh5DP8gMfZQxE44Nsp6H0/Szfkss5UmFwBEpHjl1GJMWDnB3u2d+l1CSkLoB6C+diAUlY6wL/VwJBeMPHZTf6amQIS2B/lo/CnvV/E3k=|uuoY4b7xwMYBNIZi85KBsaHmNqtJl5FrKxZI9ugeNwc=";
|
||||
|
||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId
|
||||
);
|
||||
const mockEncUserKey =
|
||||
"4.Xht6K9GA9jKcSNy4TaIvdj7f9+WsgQycs/HdkrJi33aC//roKkjf3UTGpdzFLxVP3WhyOVGyo9f2Jymf1MFPdpg7AuMnpGJlcrWLDbnPjOJo4x5gUwwBUmy3nFw6+wamyS1LRmrBPcv56yKpf80k5Q3hUrum8q9YS9m2I10vklX/TaB1YML0yo+K1feWUxg8vIx+vloxhUdkkysvcV5xU3R+AgYLrwvJS8TLL7Ug/P5HxinCaIroRrNe8xcv84vyVnzPFdXe0cfZ0cpcrm586LwfEXP2seeldO/bC51Uk/mudeSALJURPC64f5ch2cOvk48GOTapGnssCqr6ky5yFw==";
|
||||
|
||||
const userDecryptionOptsServerResponseWithTdeOption: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: true,
|
||||
TrustedDeviceOption: {
|
||||
HasAdminApproval: true,
|
||||
HasLoginApprovingDevice: true,
|
||||
HasManageResetPasswordPermission: false,
|
||||
EncryptedPrivateKey: mockEncDevicePrivateKey,
|
||||
EncryptedUserKey: mockEncUserKey,
|
||||
},
|
||||
};
|
||||
|
||||
const mockIdTokenResponseWithModifiedTrustedDeviceOption = (key: string, value: any) => {
|
||||
const userDecryptionOpts: IUserDecryptionOptionsServerResponse = {
|
||||
...userDecryptionOptsServerResponseWithTdeOption,
|
||||
TrustedDeviceOption: {
|
||||
...userDecryptionOptsServerResponseWithTdeOption.TrustedDeviceOption,
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
return identityTokenResponseFactory(null, userDecryptionOpts);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("decrypts and sets user key when trusted device decryption option exists with valid device key and enc key data", async () => {
|
||||
// Arrange
|
||||
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
|
||||
null,
|
||||
userDecryptionOptsServerResponseWithTdeOption
|
||||
);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
|
||||
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey);
|
||||
|
||||
const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey");
|
||||
|
||||
// Act
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(deviceTrustCryptoService.getDeviceKey).toHaveBeenCalledTimes(1);
|
||||
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey);
|
||||
});
|
||||
|
||||
it("does not set the user key when deviceKey is missing", async () => {
|
||||
// Arrange
|
||||
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
|
||||
null,
|
||||
userDecryptionOptsServerResponseWithTdeOption
|
||||
);
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
// Set deviceKey to be null
|
||||
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(null);
|
||||
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey);
|
||||
|
||||
// Act
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{
|
||||
valueName: "encDevicePrivateKey",
|
||||
},
|
||||
{
|
||||
valueName: "encUserKey",
|
||||
},
|
||||
])("given trusted device decryption option has missing encrypted key data", ({ valueName }) => {
|
||||
it(`does not set the user key when ${valueName} is missing`, async () => {
|
||||
// Arrange
|
||||
const idTokenResponse = mockIdTokenResponseWithModifiedTrustedDeviceOption(valueName, null);
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
// Act
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not set user key when decrypted user key is null", async () => {
|
||||
// Arrange
|
||||
const idTokenResponse: IdentityTokenResponse = identityTokenResponseFactory(
|
||||
null,
|
||||
userDecryptionOptsServerResponseWithTdeOption
|
||||
);
|
||||
apiService.postIdentityToken.mockResolvedValue(idTokenResponse);
|
||||
deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey);
|
||||
// Set userKey to be null
|
||||
deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Key Connector", () => {
|
||||
let tokenResponse: IdentityTokenResponse;
|
||||
beforeEach(() => {
|
||||
tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
});
|
||||
|
||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
||||
const masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
tokenResponse.key = null;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
const masterKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(64).buffer as CsprngArray
|
||||
) as MasterKey;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AuthRequestResponse } from "../../auth/models/response/auth-request.response";
|
||||
import { HttpStatusCode } from "../../enums";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -34,7 +40,10 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private keyConnectorService: KeyConnectorService
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
private authReqCryptoService: AuthRequestCryptoServiceAbstraction,
|
||||
private i18nService: I18nService
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
@@ -49,18 +58,6 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey(tokenResponse: IdentityTokenResponse) {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
if (!newSsoUser) {
|
||||
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
|
||||
} else {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: SsoLogInCredentials) {
|
||||
this.orgId = credentials.orgId;
|
||||
this.tokenRequest = new SsoTokenRequest(
|
||||
@@ -78,4 +75,155 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
|
||||
return ssoAuthResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
|
||||
// TODO: discuss how this is no longer true with TDE
|
||||
// eventually we’ll need to support migration of existing TDE users to Key Connector
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
if (!newSsoUser) {
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(tokenResponse.keyConnectorUrl);
|
||||
} else {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request)
|
||||
// so might be worth moving this logic to a common place (base login strategy or a separate service?)
|
||||
protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||
const masterKeyEncryptedUserKey = tokenResponse.key;
|
||||
|
||||
// Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users
|
||||
// on account creation and subsequent logins (confirmed or unconfirmed)
|
||||
// but that is fine for TDE so we cannot return if it is undefined
|
||||
|
||||
if (masterKeyEncryptedUserKey) {
|
||||
// set the master key encrypted user key if it exists
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
|
||||
}
|
||||
|
||||
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||
|
||||
// Note: TDE and key connector are mutually exclusive
|
||||
if (userDecryptionOptions?.trustedDeviceOption) {
|
||||
await this.trySetUserKeyWithApprovedAdminRequestIfExists();
|
||||
|
||||
const hasUserKey = await this.cryptoService.hasUserKey();
|
||||
|
||||
// Only try to set user key with device key if admin approval request was not successful
|
||||
if (!hasUserKey) {
|
||||
await this.trySetUserKeyWithDeviceKey(tokenResponse);
|
||||
}
|
||||
} else if (
|
||||
// TODO: remove tokenResponse.keyConnectorUrl when it's deprecated
|
||||
masterKeyEncryptedUserKey != null &&
|
||||
(tokenResponse.keyConnectorUrl || userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl)
|
||||
) {
|
||||
// Key connector enabled for user
|
||||
await this.trySetUserKeyWithMasterKey();
|
||||
}
|
||||
|
||||
// Note: In the traditional SSO flow with MP without key connector, the lock component
|
||||
// is responsible for deriving master key from MP entry and then decrypting the user key
|
||||
}
|
||||
|
||||
private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise<void> {
|
||||
// At this point a user could have an admin auth request that has been approved
|
||||
const adminAuthReqStorable = await this.stateService.getAdminAuthRequest();
|
||||
|
||||
if (!adminAuthReqStorable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call server to see if admin auth request has been approved
|
||||
let adminAuthReqResponse: AuthRequestResponse;
|
||||
|
||||
try {
|
||||
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||
// if we get a 404, it means the auth request has been deleted so clear it from storage
|
||||
await this.stateService.setAdminAuthRequest(null);
|
||||
}
|
||||
|
||||
// Always return on an error here as we don't want to block the user from logging in
|
||||
return;
|
||||
}
|
||||
|
||||
if (adminAuthReqResponse?.requestApproved) {
|
||||
// if masterPasswordHash has a value, we will always receive authReqResponse.key
|
||||
// as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
|
||||
if (adminAuthReqResponse.masterPasswordHash) {
|
||||
await this.authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
adminAuthReqResponse,
|
||||
adminAuthReqStorable.privateKey
|
||||
);
|
||||
} else {
|
||||
// if masterPasswordHash is null, we will always receive authReqResponse.key
|
||||
// as authRequestPublicKey(userKey)
|
||||
await this.authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
|
||||
adminAuthReqResponse,
|
||||
adminAuthReqStorable.privateKey
|
||||
);
|
||||
}
|
||||
|
||||
if (await this.cryptoService.hasUserKey()) {
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
await this.deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
|
||||
// if we successfully decrypted the user key, we can delete the admin auth request out of state
|
||||
// TODO: eventually we post and clean up DB as well once consumed on client
|
||||
await this.stateService.setAdminAuthRequest(null);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||
const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption;
|
||||
|
||||
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey();
|
||||
const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey;
|
||||
const encUserKey = trustedDeviceOption?.encryptedUserKey;
|
||||
|
||||
if (!deviceKey || !encDevicePrivateKey || !encUserKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||
encDevicePrivateKey,
|
||||
encUserKey,
|
||||
deviceKey
|
||||
);
|
||||
|
||||
if (userKey) {
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
|
||||
if (!masterKey) {
|
||||
throw new Error("Master key not found");
|
||||
}
|
||||
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise<void> {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -101,7 +107,18 @@ describe("UserApiLogInStrategy", () => {
|
||||
expect(stateService.addAccount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets and sets the Key Connector key from environmentUrl", async () => {
|
||||
it("sets the encrypted user key and private key from the identity token response", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
|
||||
it("gets and sets the master key if Key Connector is enabled", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
@@ -110,6 +127,24 @@ describe("UserApiLogInStrategy", () => {
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
|
||||
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
cryptoService.getMasterKey.mockResolvedValue(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,14 +44,7 @@ export class UserApiLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async setUserKey(tokenResponse: IdentityTokenResponse) {
|
||||
if (tokenResponse.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: UserApiLogInCredentials) {
|
||||
override async logIn(credentials: UserApiLogInCredentials) {
|
||||
this.tokenRequest = new UserApiTokenRequest(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
@@ -63,6 +56,31 @@ export class UserApiLogInStrategy extends LogInStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
if (response.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
|
||||
if (response.apiUseKeyConnector) {
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
|
||||
await this.cryptoService.setPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
await super.saveAccountInformation(tokenResponse);
|
||||
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
|
||||
// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future
|
||||
// type StorageShape {
|
||||
// id: string;
|
||||
// privateKey: string;
|
||||
// }
|
||||
// so we can get rid of the any type passed into fromJSON and coming out of ToJSON
|
||||
export class AdminAuthRequestStorable {
|
||||
id: string;
|
||||
privateKey: Uint8Array;
|
||||
|
||||
constructor(init?: Partial<AdminAuthRequestStorable>) {
|
||||
if (init) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
privateKey: Utils.fromBufferToByteString(this.privateKey),
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(obj: any): AdminAuthRequestStorable {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let privateKeyBuffer = null;
|
||||
if (obj.privateKey) {
|
||||
privateKeyBuffer = Utils.fromByteStringToArray(obj.privateKey);
|
||||
}
|
||||
|
||||
return new AdminAuthRequestStorable({
|
||||
id: obj.id,
|
||||
privateKey: privateKeyBuffer,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,14 @@ import { ForceResetPasswordReason } from "./force-reset-password-reason";
|
||||
|
||||
export class AuthResult {
|
||||
captchaSiteKey = "";
|
||||
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
||||
/**
|
||||
* @deprecated
|
||||
* Replace with using AccountDecryptionOptions to determine if the user does
|
||||
* not have a master password and is not using Key Connector.
|
||||
* */
|
||||
resetMasterPassword = false;
|
||||
|
||||
forcePasswordReset: ForceResetPasswordReason = ForceResetPasswordReason.None;
|
||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey, UserKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthenticationType } from "../../enums/authentication-type";
|
||||
import { TokenTwoFactorRequest } from "../request/identity-token/token-two-factor.request";
|
||||
|
||||
@@ -38,8 +38,9 @@ export class PasswordlessLogInCredentials {
|
||||
public email: string,
|
||||
public accessCode: string,
|
||||
public authRequestId: string,
|
||||
public decKey: SymmetricCryptoKey,
|
||||
public localPasswordHash: string,
|
||||
public decryptedUserKey: UserKey,
|
||||
public decryptedMasterKey: MasterKey,
|
||||
public decryptedMasterKeyHash: string,
|
||||
public twoFactor?: TokenTwoFactorRequest
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export class KeyConnectorUserDecryptionOption {
|
||||
constructor(public keyConnectorUrl: string) {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class TrustedDeviceUserDecryptionOption {
|
||||
constructor(
|
||||
public hasAdminApproval: boolean,
|
||||
public hasLoginApprovingDevice: boolean,
|
||||
public hasManageResetPasswordPermission: boolean
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
|
||||
export class UpdateDevicesTrustRequest extends SecretVerificationRequest {
|
||||
currentDevice: DeviceKeysUpdateRequest;
|
||||
otherDevices: OtherDeviceKeysUpdateRequest[];
|
||||
}
|
||||
|
||||
export class DeviceKeysUpdateRequest {
|
||||
encryptedPublicKey: string;
|
||||
encryptedUserKey: string;
|
||||
}
|
||||
|
||||
export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest {
|
||||
id: string;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { KdfType } from "../../../enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
|
||||
import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-decryption-options.response";
|
||||
|
||||
export class IdentityTokenResponse extends BaseResponse {
|
||||
accessToken: string;
|
||||
@@ -22,6 +23,8 @@ export class IdentityTokenResponse extends BaseResponse {
|
||||
apiUseKeyConnector: boolean;
|
||||
keyConnectorUrl: string;
|
||||
|
||||
userDecryptionOptions: UserDecryptionOptionsResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.accessToken = response.access_token;
|
||||
@@ -43,5 +46,11 @@ export class IdentityTokenResponse extends BaseResponse {
|
||||
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
|
||||
this.getResponseProperty("MasterPasswordPolicy")
|
||||
);
|
||||
|
||||
if (response.UserDecryptionOptions) {
|
||||
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
|
||||
this.getResponseProperty("UserDecryptionOptions")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeviceType } from "../../../enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
|
||||
export class ProtectedDeviceResponse extends BaseResponse {
|
||||
constructor(response: Jsonify<ProtectedDeviceResponse>) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("id");
|
||||
this.name = this.getResponseProperty("name");
|
||||
this.identifier = this.getResponseProperty("identifier");
|
||||
this.type = this.getResponseProperty("type");
|
||||
this.creationDate = new Date(this.getResponseProperty("creationDate"));
|
||||
if (response.encryptedUserKey) {
|
||||
this.encryptedUserKey = new EncString(this.getResponseProperty("encryptedUserKey"));
|
||||
}
|
||||
if (response.encryptedPublicKey) {
|
||||
this.encryptedPublicKey = new EncString(this.getResponseProperty("encryptedPublicKey"));
|
||||
}
|
||||
}
|
||||
|
||||
id: string;
|
||||
name: string;
|
||||
type: DeviceType;
|
||||
identifier: string;
|
||||
creationDate: Date;
|
||||
/**
|
||||
* Intended to be the users symmetric key that is encrypted in some form, the current way to encrypt this is with
|
||||
* the devices public key.
|
||||
*/
|
||||
encryptedUserKey: EncString;
|
||||
/**
|
||||
* Intended to be the public key that was generated for a device upon trust and encrypted. Currenly encrypted using
|
||||
* a users symmetric key so that when trusted and unlocked a user can decrypt the public key for all their devices.
|
||||
* This enabled a user to rotate the keys for all of their devices.
|
||||
*/
|
||||
encryptedPublicKey: EncString;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
export interface IKeyConnectorUserDecryptionOptionServerResponse {
|
||||
KeyConnectorUrl: string;
|
||||
}
|
||||
|
||||
export class KeyConnectorUserDecryptionOptionResponse extends BaseResponse {
|
||||
keyConnectorUrl: string;
|
||||
|
||||
constructor(response: IKeyConnectorUserDecryptionOptionServerResponse) {
|
||||
super(response);
|
||||
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
import { EncString } from "../../../../platform/models/domain/enc-string";
|
||||
|
||||
export interface ITrustedDeviceUserDecryptionOptionServerResponse {
|
||||
HasAdminApproval: boolean;
|
||||
HasLoginApprovingDevice: boolean;
|
||||
HasManageResetPasswordPermission: boolean;
|
||||
EncryptedPrivateKey?: string;
|
||||
EncryptedUserKey?: string;
|
||||
}
|
||||
|
||||
export class TrustedDeviceUserDecryptionOptionResponse extends BaseResponse {
|
||||
hasAdminApproval: boolean;
|
||||
hasLoginApprovingDevice: boolean;
|
||||
hasManageResetPasswordPermission: boolean;
|
||||
encryptedPrivateKey: EncString;
|
||||
encryptedUserKey: EncString;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.hasAdminApproval = this.getResponseProperty("HasAdminApproval");
|
||||
|
||||
this.hasLoginApprovingDevice = this.getResponseProperty("HasLoginApprovingDevice");
|
||||
this.hasManageResetPasswordPermission = this.getResponseProperty(
|
||||
"HasManageResetPasswordPermission"
|
||||
);
|
||||
|
||||
if (response.EncryptedPrivateKey) {
|
||||
this.encryptedPrivateKey = new EncString(this.getResponseProperty("EncryptedPrivateKey"));
|
||||
}
|
||||
if (response.EncryptedUserKey) {
|
||||
this.encryptedUserKey = new EncString(this.getResponseProperty("EncryptedUserKey"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
import {
|
||||
IKeyConnectorUserDecryptionOptionServerResponse,
|
||||
KeyConnectorUserDecryptionOptionResponse,
|
||||
} from "./key-connector-user-decryption-option.response";
|
||||
import {
|
||||
ITrustedDeviceUserDecryptionOptionServerResponse,
|
||||
TrustedDeviceUserDecryptionOptionResponse,
|
||||
} from "./trusted-device-user-decryption-option.response";
|
||||
|
||||
export interface IUserDecryptionOptionsServerResponse {
|
||||
HasMasterPassword: boolean;
|
||||
TrustedDeviceOption?: ITrustedDeviceUserDecryptionOptionServerResponse;
|
||||
KeyConnectorOption?: IKeyConnectorUserDecryptionOptionServerResponse;
|
||||
}
|
||||
|
||||
export class UserDecryptionOptionsResponse extends BaseResponse {
|
||||
hasMasterPassword: boolean;
|
||||
trustedDeviceOption?: TrustedDeviceUserDecryptionOptionResponse;
|
||||
keyConnectorOption?: KeyConnectorUserDecryptionOptionResponse;
|
||||
|
||||
constructor(response: IUserDecryptionOptionsServerResponse) {
|
||||
super(response);
|
||||
|
||||
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
|
||||
|
||||
if (response.TrustedDeviceOption) {
|
||||
this.trustedDeviceOption = new TrustedDeviceUserDecryptionOptionResponse(
|
||||
this.getResponseProperty("TrustedDeviceOption")
|
||||
);
|
||||
}
|
||||
if (response.KeyConnectorOption) {
|
||||
this.keyConnectorOption = new KeyConnectorUserDecryptionOptionResponse(
|
||||
this.getResponseProperty("KeyConnectorOption")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
UserKey,
|
||||
SymmetricCryptoKey,
|
||||
MasterKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
|
||||
import { AuthRequestResponse } from "../models/response/auth-request.response";
|
||||
|
||||
export class AuthRequestCryptoServiceImplementation implements AuthRequestCryptoServiceAbstraction {
|
||||
constructor(private cryptoService: CryptoService) {}
|
||||
|
||||
async setUserKeyAfterDecryptingSharedUserKey(
|
||||
authReqResponse: AuthRequestResponse,
|
||||
authReqPrivateKey: Uint8Array
|
||||
) {
|
||||
const userKey = await this.decryptPubKeyEncryptedUserKey(
|
||||
authReqResponse.key,
|
||||
authReqPrivateKey
|
||||
);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
|
||||
async setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
authReqResponse: AuthRequestResponse,
|
||||
authReqPrivateKey: Uint8Array
|
||||
) {
|
||||
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
|
||||
authReqResponse.key,
|
||||
authReqResponse.masterPasswordHash,
|
||||
authReqPrivateKey
|
||||
);
|
||||
|
||||
// Decrypt and set user key in state
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
|
||||
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
await this.cryptoService.setMasterKeyHash(masterKeyHash);
|
||||
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
|
||||
// Decryption helpers
|
||||
async decryptPubKeyEncryptedUserKey(
|
||||
pubKeyEncryptedUserKey: string,
|
||||
privateKey: Uint8Array
|
||||
): Promise<UserKey> {
|
||||
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
|
||||
pubKeyEncryptedUserKey,
|
||||
privateKey
|
||||
);
|
||||
|
||||
return new SymmetricCryptoKey(decryptedUserKeyBytes) as UserKey;
|
||||
}
|
||||
|
||||
async decryptPubKeyEncryptedMasterKeyAndHash(
|
||||
pubKeyEncryptedMasterKey: string,
|
||||
pubKeyEncryptedMasterKeyHash: string,
|
||||
privateKey: Uint8Array
|
||||
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
|
||||
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
|
||||
pubKeyEncryptedMasterKey,
|
||||
privateKey
|
||||
);
|
||||
|
||||
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
|
||||
pubKeyEncryptedMasterKeyHash,
|
||||
privateKey
|
||||
);
|
||||
|
||||
const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey;
|
||||
const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer);
|
||||
|
||||
return {
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
|
||||
import { AuthRequestResponse } from "../models/response/auth-request.response";
|
||||
|
||||
import { AuthRequestCryptoServiceImplementation } from "./auth-request-crypto.service.implementation";
|
||||
|
||||
describe("AuthRequestCryptoService", () => {
|
||||
let authReqCryptoService: AuthRequestCryptoServiceAbstraction;
|
||||
const cryptoService = mock<CryptoService>();
|
||||
let mockPrivateKey: Uint8Array;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
|
||||
authReqCryptoService = new AuthRequestCryptoServiceImplementation(cryptoService);
|
||||
|
||||
mockPrivateKey = new Uint8Array(64);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(authReqCryptoService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setUserKeyAfterDecryptingSharedUserKey", () => {
|
||||
it("decrypts and sets user key when given valid auth request response and private key", async () => {
|
||||
// Arrange
|
||||
const mockAuthReqResponse = {
|
||||
key: "authReqPublicKeyEncryptedUserKey",
|
||||
} as AuthRequestResponse;
|
||||
|
||||
const mockDecryptedUserKey = {} as UserKey;
|
||||
jest
|
||||
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedUserKey")
|
||||
.mockResolvedValueOnce(mockDecryptedUserKey);
|
||||
|
||||
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
|
||||
|
||||
// Act
|
||||
await authReqCryptoService.setUserKeyAfterDecryptingSharedUserKey(
|
||||
mockAuthReqResponse,
|
||||
mockPrivateKey
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(authReqCryptoService.decryptPubKeyEncryptedUserKey).toBeCalledWith(
|
||||
mockAuthReqResponse.key,
|
||||
mockPrivateKey
|
||||
);
|
||||
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setKeysAfterDecryptingSharedMasterKeyAndHash", () => {
|
||||
it("decrypts and sets master key and hash and user key when given valid auth request response and private key", async () => {
|
||||
// Arrange
|
||||
const mockAuthReqResponse = {
|
||||
key: "authReqPublicKeyEncryptedMasterKey",
|
||||
masterPasswordHash: "authReqPublicKeyEncryptedMasterKeyHash",
|
||||
} as AuthRequestResponse;
|
||||
|
||||
const mockDecryptedMasterKey = {} as MasterKey;
|
||||
const mockDecryptedMasterKeyHash = "mockDecryptedMasterKeyHash";
|
||||
const mockDecryptedUserKey = {} as UserKey;
|
||||
|
||||
jest
|
||||
.spyOn(authReqCryptoService, "decryptPubKeyEncryptedMasterKeyAndHash")
|
||||
.mockResolvedValueOnce({
|
||||
masterKey: mockDecryptedMasterKey,
|
||||
masterKeyHash: mockDecryptedMasterKeyHash,
|
||||
});
|
||||
|
||||
cryptoService.setMasterKey.mockResolvedValueOnce(undefined);
|
||||
cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
|
||||
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
|
||||
|
||||
// Act
|
||||
await authReqCryptoService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
mockAuthReqResponse,
|
||||
mockPrivateKey
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash).toBeCalledWith(
|
||||
mockAuthReqResponse.key,
|
||||
mockAuthReqResponse.masterPasswordHash,
|
||||
mockPrivateKey
|
||||
);
|
||||
expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey);
|
||||
expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash);
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey);
|
||||
expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptAuthReqPubKeyEncryptedUserKey", () => {
|
||||
it("returns a decrypted user key when given valid public key encrypted user key and an auth req private key", async () => {
|
||||
// Arrange
|
||||
const mockPubKeyEncryptedUserKey = "pubKeyEncryptedUserKey";
|
||||
const mockDecryptedUserKeyBytes = new Uint8Array(64);
|
||||
const mockDecryptedUserKey = new SymmetricCryptoKey(mockDecryptedUserKeyBytes) as UserKey;
|
||||
|
||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedUserKeyBytes);
|
||||
|
||||
// Act
|
||||
const result = await authReqCryptoService.decryptPubKeyEncryptedUserKey(
|
||||
mockPubKeyEncryptedUserKey,
|
||||
mockPrivateKey
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.rsaDecrypt).toBeCalledWith(mockPubKeyEncryptedUserKey, mockPrivateKey);
|
||||
expect(result).toEqual(mockDecryptedUserKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptAuthReqPubKeyEncryptedMasterKeyAndHash", () => {
|
||||
it("returns a decrypted master key and hash when given a valid public key encrypted master key, public key encrypted master key hash, and an auth req private key", async () => {
|
||||
// Arrange
|
||||
const mockPubKeyEncryptedMasterKey = "pubKeyEncryptedMasterKey";
|
||||
const mockPubKeyEncryptedMasterKeyHash = "pubKeyEncryptedMasterKeyHash";
|
||||
|
||||
const mockDecryptedMasterKeyBytes = new Uint8Array(64);
|
||||
const mockDecryptedMasterKey = new SymmetricCryptoKey(
|
||||
mockDecryptedMasterKeyBytes
|
||||
) as MasterKey;
|
||||
const mockDecryptedMasterKeyHashBytes = new Uint8Array(64);
|
||||
const mockDecryptedMasterKeyHash = Utils.fromBufferToUtf8(mockDecryptedMasterKeyHashBytes);
|
||||
|
||||
cryptoService.rsaDecrypt
|
||||
.mockResolvedValueOnce(mockDecryptedMasterKeyBytes)
|
||||
.mockResolvedValueOnce(mockDecryptedMasterKeyHashBytes);
|
||||
|
||||
// Act
|
||||
const result = await authReqCryptoService.decryptPubKeyEncryptedMasterKeyAndHash(
|
||||
mockPubKeyEncryptedMasterKey,
|
||||
mockPubKeyEncryptedMasterKeyHash,
|
||||
mockPrivateKey
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
mockPubKeyEncryptedMasterKey,
|
||||
mockPrivateKey
|
||||
);
|
||||
expect(cryptoService.rsaDecrypt).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
mockPubKeyEncryptedMasterKeyHash,
|
||||
mockPrivateKey
|
||||
);
|
||||
expect(result.masterKey).toEqual(mockDecryptedMasterKey);
|
||||
expect(result.masterKeyHash).toEqual(mockDecryptedMasterKeyHash);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,9 +16,11 @@ import { MessagingService } from "../../platform/abstractions/messaging.service"
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "../../tools/password-strength";
|
||||
import { AuthRequestCryptoServiceAbstraction } from "../abstractions/auth-request-crypto.service.abstraction";
|
||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { KeyConnectorService } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { TwoFactorService } from "../abstractions/two-factor.service";
|
||||
@@ -103,7 +105,9 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
protected i18nService: I18nService,
|
||||
protected encryptService: EncryptService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected policyService: PolicyService
|
||||
protected policyService: PolicyService,
|
||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||
protected authReqCryptoService: AuthRequestCryptoServiceAbstraction
|
||||
) {}
|
||||
|
||||
async logIn(
|
||||
@@ -149,7 +153,10 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.keyConnectorService
|
||||
this.keyConnectorService,
|
||||
this.deviceTrustCryptoService,
|
||||
this.authReqCryptoService,
|
||||
this.i18nService
|
||||
);
|
||||
break;
|
||||
case AuthenticationType.UserApi:
|
||||
@@ -178,7 +185,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this
|
||||
this.deviceTrustCryptoService
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -238,23 +245,32 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
}
|
||||
|
||||
async getAuthStatus(userId?: string): Promise<AuthenticationStatus> {
|
||||
// If we don't have an access token or userId, we're logged out
|
||||
const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId });
|
||||
if (!isAuthenticated) {
|
||||
return AuthenticationStatus.LoggedOut;
|
||||
}
|
||||
|
||||
// Keys aren't stored for a device that is locked or logged out
|
||||
// Make sure we're logged in before checking this, otherwise we could mix up those states
|
||||
const neverLock =
|
||||
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
|
||||
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
|
||||
if (neverLock) {
|
||||
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
|
||||
// We should refactor here.
|
||||
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
|
||||
// If we don't have a user key in memory, we're locked
|
||||
if (!(await this.cryptoService.hasUserKeyInMemory(userId))) {
|
||||
// Check if the user has vault timeout set to never and verify that
|
||||
// they've never unlocked their vault
|
||||
const neverLock =
|
||||
(await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) &&
|
||||
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
|
||||
|
||||
if (neverLock) {
|
||||
// Attempt to get the key from storage and set it in memory
|
||||
const userKey = await this.cryptoService.getUserKeyFromStorage(
|
||||
KeySuffixOptions.Auto,
|
||||
userId
|
||||
);
|
||||
await this.cryptoService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
const hasKeyInMemory = await this.cryptoService.hasKeyInMemory(userId);
|
||||
// We do another check here in case setting the auto key failed
|
||||
const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId);
|
||||
if (!hasKeyInMemory) {
|
||||
return AuthenticationStatus.Locked;
|
||||
}
|
||||
@@ -262,7 +278,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
return AuthenticationStatus.Unlocked;
|
||||
}
|
||||
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
|
||||
email = email.trim().toLowerCase();
|
||||
let kdf: KdfType = null;
|
||||
let kdfConfig: KdfConfig = null;
|
||||
@@ -281,7 +297,7 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfConfig);
|
||||
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
|
||||
}
|
||||
|
||||
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> {
|
||||
@@ -298,22 +314,33 @@ export class AuthService implements AuthServiceAbstraction {
|
||||
requestApproved: boolean
|
||||
): Promise<AuthRequestResponse> {
|
||||
const pubKey = Utils.fromB64ToArray(key);
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(
|
||||
(
|
||||
await this.cryptoService.getKey()
|
||||
).encKey,
|
||||
pubKey
|
||||
);
|
||||
let encryptedMasterPassword = null;
|
||||
if ((await this.stateService.getKeyHash()) != null) {
|
||||
encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
|
||||
pubKey
|
||||
);
|
||||
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
let keyToEncrypt;
|
||||
let encryptedMasterKeyHash = null;
|
||||
|
||||
if (masterKey) {
|
||||
keyToEncrypt = masterKey.encKey;
|
||||
|
||||
// Only encrypt the master password hash if masterKey exists as
|
||||
// we won't have a masterKeyHash without a masterKey
|
||||
const masterKeyHash = await this.stateService.getKeyHash();
|
||||
if (masterKeyHash != null) {
|
||||
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(masterKeyHash),
|
||||
pubKey
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
keyToEncrypt = userKey.key;
|
||||
}
|
||||
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
|
||||
|
||||
const request = new PasswordlessAuthRequest(
|
||||
encryptedKey.encryptedString,
|
||||
encryptedMasterPassword?.encryptedString,
|
||||
encryptedMasterKeyHash?.encryptedString,
|
||||
await this.appIdService.getAppId(),
|
||||
requestApproved
|
||||
);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import {
|
||||
SymmetricCryptoKey,
|
||||
DeviceKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import {
|
||||
DeviceKeysUpdateRequest,
|
||||
UpdateDevicesTrustRequest,
|
||||
} from "../models/request/update-devices-trust.request";
|
||||
|
||||
export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction {
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private cryptoService: CryptoService,
|
||||
private encryptService: EncryptService,
|
||||
private stateService: StateService,
|
||||
private appIdService: AppIdService,
|
||||
private devicesApiService: DevicesApiServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @description Retrieves the users choice to trust the device which can only happen after decryption
|
||||
* Note: this value should only be used once and then reset
|
||||
*/
|
||||
async getShouldTrustDevice(): Promise<boolean> {
|
||||
return await this.stateService.getShouldTrustDevice();
|
||||
}
|
||||
|
||||
async setShouldTrustDevice(value: boolean): Promise<void> {
|
||||
await this.stateService.setShouldTrustDevice(value);
|
||||
}
|
||||
|
||||
async trustDeviceIfRequired(): Promise<void> {
|
||||
const shouldTrustDevice = await this.getShouldTrustDevice();
|
||||
if (shouldTrustDevice) {
|
||||
await this.trustDevice();
|
||||
// reset the trust choice
|
||||
await this.setShouldTrustDevice(false);
|
||||
}
|
||||
}
|
||||
|
||||
async trustDevice(): Promise<DeviceResponse> {
|
||||
// Attempt to get user key
|
||||
const userKey: UserKey = await this.cryptoService.getUserKey();
|
||||
|
||||
// If user key is not found, throw error
|
||||
if (!userKey) {
|
||||
throw new Error("User symmetric key not found");
|
||||
}
|
||||
|
||||
// Generate deviceKey
|
||||
const deviceKey = await this.makeDeviceKey();
|
||||
|
||||
// Generate asymmetric RSA key pair: devicePrivateKey, devicePublicKey
|
||||
const [devicePublicKey, devicePrivateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(
|
||||
2048
|
||||
);
|
||||
|
||||
const [
|
||||
devicePublicKeyEncryptedUserKey,
|
||||
userKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey,
|
||||
] = await Promise.all([
|
||||
// Encrypt user key with the DevicePublicKey
|
||||
this.cryptoService.rsaEncrypt(userKey.key, devicePublicKey),
|
||||
|
||||
// Encrypt devicePublicKey with user key
|
||||
this.encryptService.encrypt(devicePublicKey, userKey),
|
||||
|
||||
// Encrypt devicePrivateKey with deviceKey
|
||||
this.encryptService.encrypt(devicePrivateKey, deviceKey),
|
||||
]);
|
||||
|
||||
// Send encrypted keys to server
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const deviceResponse = await this.devicesApiService.updateTrustedDeviceKeys(
|
||||
deviceIdentifier,
|
||||
devicePublicKeyEncryptedUserKey.encryptedString,
|
||||
userKeyEncryptedDevicePublicKey.encryptedString,
|
||||
deviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
|
||||
// store device key in local/secure storage if enc keys posted to server successfully
|
||||
await this.setDeviceKey(deviceKey);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
|
||||
|
||||
return deviceResponse;
|
||||
}
|
||||
|
||||
async rotateDevicesTrust(newUserKey: UserKey, masterPasswordHash: string): Promise<void> {
|
||||
const currentDeviceKey = await this.getDeviceKey();
|
||||
if (currentDeviceKey == null) {
|
||||
// If the current device doesn't have a device key available to it, then we can't
|
||||
// rotate any trust at all, so early return.
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point of rotating their keys, they should still have their old user key in state
|
||||
const oldUserKey = await this.stateService.getUserKey();
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const secretVerificationRequest = new SecretVerificationRequest();
|
||||
secretVerificationRequest.masterPasswordHash = masterPasswordHash;
|
||||
|
||||
// Get the keys that are used in rotating a devices keys from the server
|
||||
const currentDeviceKeys = await this.devicesApiService.getDeviceKeys(
|
||||
deviceIdentifier,
|
||||
secretVerificationRequest
|
||||
);
|
||||
|
||||
// Decrypt the existing device public key with the old user key
|
||||
const decryptedDevicePublicKey = await this.encryptService.decryptToBytes(
|
||||
currentDeviceKeys.encryptedPublicKey,
|
||||
oldUserKey
|
||||
);
|
||||
|
||||
// Encrypt the brand new user key with the now-decrypted public key for the device
|
||||
const encryptedNewUserKey = await this.cryptoService.rsaEncrypt(
|
||||
newUserKey.key,
|
||||
decryptedDevicePublicKey
|
||||
);
|
||||
|
||||
// Re-encrypt the device public key with the new user key
|
||||
const encryptedDevicePublicKey = await this.encryptService.encrypt(
|
||||
decryptedDevicePublicKey,
|
||||
newUserKey
|
||||
);
|
||||
|
||||
const currentDeviceUpdateRequest = new DeviceKeysUpdateRequest();
|
||||
currentDeviceUpdateRequest.encryptedUserKey = encryptedNewUserKey.encryptedString;
|
||||
currentDeviceUpdateRequest.encryptedPublicKey = encryptedDevicePublicKey.encryptedString;
|
||||
|
||||
// TODO: For device management, allow this method to take an array of device ids that can be looped over and individually rotated
|
||||
// then it can be added to trustRequest.otherDevices.
|
||||
|
||||
const trustRequest = new UpdateDevicesTrustRequest();
|
||||
trustRequest.masterPasswordHash = masterPasswordHash;
|
||||
trustRequest.currentDevice = currentDeviceUpdateRequest;
|
||||
trustRequest.otherDevices = [];
|
||||
|
||||
await this.devicesApiService.updateTrust(trustRequest);
|
||||
}
|
||||
|
||||
async getDeviceKey(): Promise<DeviceKey> {
|
||||
return await this.stateService.getDeviceKey();
|
||||
}
|
||||
|
||||
private async setDeviceKey(deviceKey: DeviceKey | null): Promise<void> {
|
||||
await this.stateService.setDeviceKey(deviceKey);
|
||||
}
|
||||
|
||||
private async makeDeviceKey(): Promise<DeviceKey> {
|
||||
// Create 512-bit device key
|
||||
const randomBytes: CsprngArray = await this.cryptoFunctionService.randomBytes(64);
|
||||
const deviceKey = new SymmetricCryptoKey(randomBytes) as DeviceKey;
|
||||
|
||||
return deviceKey;
|
||||
}
|
||||
|
||||
async decryptUserKeyWithDeviceKey(
|
||||
encryptedDevicePrivateKey: EncString,
|
||||
encryptedUserKey: EncString,
|
||||
deviceKey?: DeviceKey
|
||||
): Promise<UserKey | null> {
|
||||
// If device key provided use it, otherwise try to retrieve from storage
|
||||
deviceKey ||= await this.getDeviceKey();
|
||||
|
||||
if (!deviceKey) {
|
||||
// User doesn't have a device key anymore so device is untrusted
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// attempt to decrypt encryptedDevicePrivateKey with device key
|
||||
const devicePrivateKey = await this.encryptService.decryptToBytes(
|
||||
encryptedDevicePrivateKey,
|
||||
deviceKey
|
||||
);
|
||||
|
||||
// Attempt to decrypt encryptedUserDataKey with devicePrivateKey
|
||||
const userKey = await this.cryptoService.rsaDecrypt(
|
||||
encryptedUserKey.encryptedString,
|
||||
devicePrivateKey
|
||||
);
|
||||
|
||||
return new SymmetricCryptoKey(userKey) as UserKey;
|
||||
} catch (e) {
|
||||
// If either decryption effort fails, we want to remove the device key
|
||||
await this.setDeviceKey(null);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async supportsDeviceTrust(): Promise<boolean> {
|
||||
const decryptionOptions = await this.stateService.getAccountDecryptionOptions();
|
||||
return decryptionOptions?.trustedDeviceOption != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
import { matches, mock } from "jest-mock-extended";
|
||||
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { DeviceType } from "../../enums";
|
||||
import { EncryptionType } from "../../enums/encryption-type.enum";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import {
|
||||
SymmetricCryptoKey,
|
||||
DeviceKey,
|
||||
UserKey,
|
||||
} from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
|
||||
import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation";
|
||||
describe("deviceTrustCryptoService", () => {
|
||||
let deviceTrustCryptoService: DeviceTrustCryptoService;
|
||||
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const cryptoService = mock<CryptoService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
const stateService = mock<StateService>();
|
||||
const appIdService = mock<AppIdService>();
|
||||
const devicesApiService = mock<DevicesApiServiceAbstraction>();
|
||||
const i18nService = mock<I18nService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
deviceTrustCryptoService = new DeviceTrustCryptoService(
|
||||
cryptoFunctionService,
|
||||
cryptoService,
|
||||
encryptService,
|
||||
stateService,
|
||||
appIdService,
|
||||
devicesApiService,
|
||||
i18nService,
|
||||
platformUtilsService
|
||||
);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(deviceTrustCryptoService).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("User Trust Device Choice For Decryption", () => {
|
||||
describe("getShouldTrustDevice", () => {
|
||||
it("gets the user trust device choice for decryption from the state service", async () => {
|
||||
const stateSvcGetShouldTrustDeviceSpy = jest.spyOn(stateService, "getShouldTrustDevice");
|
||||
|
||||
const expectedValue = true;
|
||||
stateSvcGetShouldTrustDeviceSpy.mockResolvedValue(expectedValue);
|
||||
const result = await deviceTrustCryptoService.getShouldTrustDevice();
|
||||
|
||||
expect(stateSvcGetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setShouldTrustDevice", () => {
|
||||
it("sets the user trust device choice for decryption in the state service", async () => {
|
||||
const stateSvcSetShouldTrustDeviceSpy = jest.spyOn(stateService, "setShouldTrustDevice");
|
||||
|
||||
const newValue = true;
|
||||
await deviceTrustCryptoService.setShouldTrustDevice(newValue);
|
||||
|
||||
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledWith(newValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("trustDeviceIfRequired", () => {
|
||||
it("should trust device and reset when getShouldTrustDevice returns true", async () => {
|
||||
jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true);
|
||||
jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse);
|
||||
jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue();
|
||||
|
||||
await deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
|
||||
expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1);
|
||||
expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1);
|
||||
expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("should not trust device nor reset when getShouldTrustDevice returns false", async () => {
|
||||
const getShouldTrustDeviceSpy = jest
|
||||
.spyOn(deviceTrustCryptoService, "getShouldTrustDevice")
|
||||
.mockResolvedValue(false);
|
||||
const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice");
|
||||
const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice");
|
||||
|
||||
await deviceTrustCryptoService.trustDeviceIfRequired();
|
||||
|
||||
expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(trustDeviceSpy).not.toHaveBeenCalled();
|
||||
expect(setShouldTrustDeviceSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Trusted Device Encryption core logic tests", () => {
|
||||
const deviceKeyBytesLength = 64;
|
||||
const userKeyBytesLength = 64;
|
||||
|
||||
describe("getDeviceKey", () => {
|
||||
let existingDeviceKey: DeviceKey;
|
||||
let stateSvcGetDeviceKeySpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
existingDeviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength) as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
|
||||
});
|
||||
|
||||
it("returns null when there is not an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(null);
|
||||
|
||||
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(deviceKey).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the device key when there is an existing device key", async () => {
|
||||
stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey);
|
||||
|
||||
const deviceKey = await deviceTrustCryptoService.getDeviceKey();
|
||||
|
||||
expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(deviceKey).toEqual(existingDeviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDeviceKey", () => {
|
||||
it("sets the device key in the state service", async () => {
|
||||
const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey");
|
||||
|
||||
const deviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength) as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
// This is a hacky workaround, but it allows for cleaner tests
|
||||
await (deviceTrustCryptoService as any).setDeviceKey(deviceKey);
|
||||
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeDeviceKey", () => {
|
||||
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
|
||||
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
|
||||
const cryptoFuncSvcRandomBytesSpy = jest
|
||||
.spyOn(cryptoFunctionService, "randomBytes")
|
||||
.mockResolvedValue(mockRandomBytes);
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
// This is a hacky workaround, but it allows for cleaner tests
|
||||
const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey();
|
||||
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoFuncSvcRandomBytesSpy).toHaveBeenCalledWith(deviceKeyBytesLength);
|
||||
|
||||
expect(deviceKey).not.toBeNull();
|
||||
expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("trustDevice", () => {
|
||||
let mockDeviceKeyRandomBytes: CsprngArray;
|
||||
let mockDeviceKey: DeviceKey;
|
||||
|
||||
let mockUserKeyRandomBytes: CsprngArray;
|
||||
let mockUserKey: UserKey;
|
||||
|
||||
const deviceRsaKeyLength = 2048;
|
||||
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
|
||||
let mockDevicePrivateKey: Uint8Array;
|
||||
let mockDevicePublicKey: Uint8Array;
|
||||
let mockDevicePublicKeyEncryptedUserKey: EncString;
|
||||
let mockUserKeyEncryptedDevicePublicKey: EncString;
|
||||
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
|
||||
|
||||
const mockDeviceResponse: DeviceResponse = new DeviceResponse({
|
||||
Id: "mockId",
|
||||
Name: "mockName",
|
||||
Identifier: "mockIdentifier",
|
||||
Type: "mockType",
|
||||
CreationDate: "mockCreationDate",
|
||||
});
|
||||
|
||||
const mockDeviceId = "mockDeviceId";
|
||||
|
||||
let makeDeviceKeySpy: jest.SpyInstance;
|
||||
let rsaGenerateKeyPairSpy: jest.SpyInstance;
|
||||
let cryptoSvcGetUserKeySpy: jest.SpyInstance;
|
||||
let cryptoSvcRsaEncryptSpy: jest.SpyInstance;
|
||||
let encryptServiceEncryptSpy: jest.SpyInstance;
|
||||
let appIdServiceGetAppIdSpy: jest.SpyInstance;
|
||||
let devicesApiServiceUpdateTrustedDeviceKeysSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup all spies and default return values for the happy path
|
||||
|
||||
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength) as CsprngArray;
|
||||
mockUserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
|
||||
|
||||
mockDeviceRsaKeyPair = [
|
||||
new Uint8Array(deviceRsaKeyLength),
|
||||
new Uint8Array(deviceRsaKeyLength),
|
||||
];
|
||||
|
||||
mockDevicePublicKey = mockDeviceRsaKeyPair[0];
|
||||
mockDevicePrivateKey = mockDeviceRsaKeyPair[1];
|
||||
|
||||
mockDevicePublicKeyEncryptedUserKey = new EncString(
|
||||
EncryptionType.Rsa2048_OaepSha1_B64,
|
||||
"mockDevicePublicKeyEncryptedUserKey"
|
||||
);
|
||||
|
||||
mockUserKeyEncryptedDevicePublicKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockUserKeyEncryptedDevicePublicKey"
|
||||
);
|
||||
|
||||
mockDeviceKeyEncryptedDevicePrivateKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockDeviceKeyEncryptedDevicePrivateKey"
|
||||
);
|
||||
|
||||
// TypeScript will allow calling private methods if the object is of type 'any'
|
||||
makeDeviceKeySpy = jest
|
||||
.spyOn(deviceTrustCryptoService as any, "makeDeviceKey")
|
||||
.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
rsaGenerateKeyPairSpy = jest
|
||||
.spyOn(cryptoFunctionService, "rsaGenerateKeyPair")
|
||||
.mockResolvedValue(mockDeviceRsaKeyPair);
|
||||
|
||||
cryptoSvcGetUserKeySpy = jest
|
||||
.spyOn(cryptoService, "getUserKey")
|
||||
.mockResolvedValue(mockUserKey);
|
||||
|
||||
cryptoSvcRsaEncryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaEncrypt")
|
||||
.mockResolvedValue(mockDevicePublicKeyEncryptedUserKey);
|
||||
|
||||
encryptServiceEncryptSpy = jest
|
||||
.spyOn(encryptService, "encrypt")
|
||||
.mockImplementation((plainValue, key) => {
|
||||
if (plainValue === mockDevicePublicKey && key === mockUserKey) {
|
||||
return Promise.resolve(mockUserKeyEncryptedDevicePublicKey);
|
||||
}
|
||||
if (plainValue === mockDevicePrivateKey && key === mockDeviceKey) {
|
||||
return Promise.resolve(mockDeviceKeyEncryptedDevicePrivateKey);
|
||||
}
|
||||
});
|
||||
|
||||
appIdServiceGetAppIdSpy = jest
|
||||
.spyOn(appIdService, "getAppId")
|
||||
.mockResolvedValue(mockDeviceId);
|
||||
|
||||
devicesApiServiceUpdateTrustedDeviceKeysSpy = jest
|
||||
.spyOn(devicesApiService, "updateTrustedDeviceKeys")
|
||||
.mockResolvedValue(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => {
|
||||
const response = await deviceTrustCryptoService.trustDevice();
|
||||
|
||||
expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoSvcGetUserKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(cryptoSvcRsaEncryptSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// RsaEncrypt must be called w/ a user key array buffer of 64 bytes
|
||||
const userKeyKey: Uint8Array = cryptoSvcRsaEncryptSpy.mock.calls[0][0];
|
||||
expect(userKeyKey.byteLength).toBe(64);
|
||||
|
||||
expect(encryptServiceEncryptSpy).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(appIdServiceGetAppIdSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledTimes(1);
|
||||
expect(devicesApiServiceUpdateTrustedDeviceKeysSpy).toHaveBeenCalledWith(
|
||||
mockDeviceId,
|
||||
mockDevicePublicKeyEncryptedUserKey.encryptedString,
|
||||
mockUserKeyEncryptedDevicePublicKey.encryptedString,
|
||||
mockDeviceKeyEncryptedDevicePrivateKey.encryptedString
|
||||
);
|
||||
|
||||
expect(response).toBeInstanceOf(DeviceResponse);
|
||||
expect(response).toEqual(mockDeviceResponse);
|
||||
});
|
||||
|
||||
it("throws specific error if user key is not found", async () => {
|
||||
// setup the spy to return null
|
||||
cryptoSvcGetUserKeySpy.mockResolvedValue(null);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
|
||||
// reset the spy
|
||||
cryptoSvcGetUserKeySpy.mockReset();
|
||||
|
||||
// setup the spy to return undefined
|
||||
cryptoSvcGetUserKeySpy.mockResolvedValue(undefined);
|
||||
// check if the expected error is thrown
|
||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(
|
||||
"User symmetric key not found"
|
||||
);
|
||||
});
|
||||
|
||||
const methodsToTestForErrorsOrInvalidReturns: any = [
|
||||
{
|
||||
method: "makeDeviceKey",
|
||||
spy: () => makeDeviceKeySpy,
|
||||
errorText: "makeDeviceKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaGenerateKeyPair",
|
||||
spy: () => rsaGenerateKeyPairSpy,
|
||||
errorText: "rsaGenerateKeyPair error",
|
||||
},
|
||||
{
|
||||
method: "getUserKey",
|
||||
spy: () => cryptoSvcGetUserKeySpy,
|
||||
errorText: "getUserKey error",
|
||||
},
|
||||
{
|
||||
method: "rsaEncrypt",
|
||||
spy: () => cryptoSvcRsaEncryptSpy,
|
||||
errorText: "rsaEncrypt error",
|
||||
},
|
||||
{
|
||||
method: "encryptService.encrypt",
|
||||
spy: () => encryptServiceEncryptSpy,
|
||||
errorText: "encryptService.encrypt error",
|
||||
},
|
||||
];
|
||||
|
||||
describe.each(methodsToTestForErrorsOrInvalidReturns)(
|
||||
"trustDevice error handling and invalid return testing",
|
||||
({ method, spy, errorText }) => {
|
||||
// ensures that error propagation works correctly
|
||||
it(`throws an error if ${method} fails`, async () => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockRejectedValue(new Error(errorText));
|
||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(errorText);
|
||||
});
|
||||
|
||||
test.each([null, undefined])(
|
||||
`throws an error if ${method} returns %s`,
|
||||
async (invalidValue) => {
|
||||
const methodSpy = spy();
|
||||
methodSpy.mockResolvedValue(invalidValue);
|
||||
await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("decryptUserKeyWithDeviceKey", () => {
|
||||
let mockDeviceKey: DeviceKey;
|
||||
let mockEncryptedDevicePrivateKey: EncString;
|
||||
let mockEncryptedUserKey: EncString;
|
||||
let mockUserKey: UserKey;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
|
||||
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
|
||||
|
||||
const mockUserKeyRandomBytes = new Uint8Array(userKeyBytesLength) as CsprngArray;
|
||||
mockUserKey = new SymmetricCryptoKey(mockUserKeyRandomBytes) as UserKey;
|
||||
|
||||
mockEncryptedDevicePrivateKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockEncryptedDevicePrivateKey"
|
||||
);
|
||||
|
||||
mockEncryptedUserKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
"mockEncryptedUserKey"
|
||||
);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("returns null when device key isn't provided and isn't in state", async () => {
|
||||
const getDeviceKeySpy = jest
|
||||
.spyOn(deviceTrustCryptoService, "getDeviceKey")
|
||||
.mockResolvedValue(null);
|
||||
|
||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||
mockEncryptedDevicePrivateKey,
|
||||
mockEncryptedUserKey
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
|
||||
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("successfully returns the user key when provided keys (including device key) can decrypt it", async () => {
|
||||
const decryptToBytesSpy = jest
|
||||
.spyOn(encryptService, "decryptToBytes")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
const rsaDecryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaDecrypt")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
|
||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||
mockEncryptedDevicePrivateKey,
|
||||
mockEncryptedUserKey,
|
||||
mockDeviceKey
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockUserKey);
|
||||
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("successfully returns the user key when a device key is not provided (retrieves device key from state)", async () => {
|
||||
const getDeviceKeySpy = jest
|
||||
.spyOn(deviceTrustCryptoService, "getDeviceKey")
|
||||
.mockResolvedValue(mockDeviceKey);
|
||||
|
||||
const decryptToBytesSpy = jest
|
||||
.spyOn(encryptService, "decryptToBytes")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
const rsaDecryptSpy = jest
|
||||
.spyOn(cryptoService, "rsaDecrypt")
|
||||
.mockResolvedValue(new Uint8Array(userKeyBytesLength));
|
||||
|
||||
// Call without providing a device key
|
||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||
mockEncryptedDevicePrivateKey,
|
||||
mockEncryptedUserKey
|
||||
);
|
||||
|
||||
expect(getDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(result).toEqual(mockUserKey);
|
||||
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(rsaDecryptSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns null and removes device key when the decryption fails", async () => {
|
||||
const decryptToBytesSpy = jest
|
||||
.spyOn(encryptService, "decryptToBytes")
|
||||
.mockRejectedValue(new Error("Decryption error"));
|
||||
const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey");
|
||||
|
||||
const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
|
||||
mockEncryptedDevicePrivateKey,
|
||||
mockEncryptedUserKey,
|
||||
mockDeviceKey
|
||||
);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(decryptToBytesSpy).toHaveBeenCalledTimes(1);
|
||||
expect(setDeviceKeySpy).toHaveBeenCalledTimes(1);
|
||||
expect(setDeviceKeySpy).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateDevicesTrust", () => {
|
||||
let fakeNewUserKey: UserKey = null;
|
||||
|
||||
const FakeNewUserKeyMarker = 1;
|
||||
const FakeOldUserKeyMarker = 5;
|
||||
const FakeDecryptedPublicKeyMarker = 17;
|
||||
|
||||
beforeEach(() => {
|
||||
const fakeNewUserKeyData = new Uint8Array(64);
|
||||
fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1);
|
||||
fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey;
|
||||
});
|
||||
|
||||
it("does an early exit when the current device is not a trusted device", async () => {
|
||||
stateService.getDeviceKey.mockResolvedValue(null);
|
||||
|
||||
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "");
|
||||
|
||||
expect(devicesApiService.updateTrust).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe("is on a trusted device", () => {
|
||||
beforeEach(() => {
|
||||
stateService.getDeviceKey.mockResolvedValue(
|
||||
new SymmetricCryptoKey(new Uint8Array(deviceKeyBytesLength)) as DeviceKey
|
||||
);
|
||||
});
|
||||
|
||||
it("rotates current device keys and calls api service when the current device is trusted", async () => {
|
||||
const currentEncryptedPublicKey = new EncString("2.cHVibGlj|cHVibGlj|cHVibGlj");
|
||||
const currentEncryptedUserKey = new EncString("4.dXNlcg==");
|
||||
|
||||
const fakeOldUserKeyData = new Uint8Array(new Uint8Array(64));
|
||||
// Fill the first byte with something identifiable
|
||||
fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1);
|
||||
|
||||
// Mock the retrieval of a user key that differs from the new one passed into the method
|
||||
stateService.getUserKey.mockResolvedValue(
|
||||
new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey
|
||||
);
|
||||
|
||||
appIdService.getAppId.mockResolvedValue("test_device_identifier");
|
||||
|
||||
devicesApiService.getDeviceKeys.mockImplementation((deviceIdentifier, secretRequest) => {
|
||||
if (
|
||||
deviceIdentifier !== "test_device_identifier" ||
|
||||
secretRequest.masterPasswordHash !== "my_password_hash"
|
||||
) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return Promise.resolve(
|
||||
new ProtectedDeviceResponse({
|
||||
id: "",
|
||||
creationDate: "",
|
||||
identifier: "test_device_identifier",
|
||||
name: "Firefox",
|
||||
type: DeviceType.FirefoxBrowser,
|
||||
encryptedPublicKey: currentEncryptedPublicKey.encryptedString,
|
||||
encryptedUserKey: currentEncryptedUserKey.encryptedString,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Mock the decryption of the public key with the old user key
|
||||
encryptService.decryptToBytes.mockImplementationOnce((_encValue, privateKeyValue) => {
|
||||
expect(privateKeyValue.key.byteLength).toBe(64);
|
||||
expect(new Uint8Array(privateKeyValue.key)[0]).toBe(FakeOldUserKeyMarker);
|
||||
const data = new Uint8Array(250);
|
||||
data.fill(FakeDecryptedPublicKeyMarker, 0, 1);
|
||||
return Promise.resolve(data);
|
||||
});
|
||||
|
||||
// Mock the encryption of the new user key with the decrypted public key
|
||||
cryptoService.rsaEncrypt.mockImplementationOnce((data, publicKey) => {
|
||||
expect(data.byteLength).toBe(64); // New key should also be 64 bytes
|
||||
expect(new Uint8Array(data)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1';
|
||||
|
||||
expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker);
|
||||
return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg=="));
|
||||
});
|
||||
|
||||
// Mock the reencryption of the device public key with the new user key
|
||||
encryptService.encrypt.mockImplementationOnce((plainValue, key) => {
|
||||
expect(plainValue).toBeInstanceOf(Uint8Array);
|
||||
expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker);
|
||||
|
||||
expect(new Uint8Array(key.key)[0]).toBe(FakeNewUserKeyMarker);
|
||||
return Promise.resolve(
|
||||
new EncString("2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj")
|
||||
);
|
||||
});
|
||||
|
||||
await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "my_password_hash");
|
||||
|
||||
expect(devicesApiService.updateTrust).toBeCalledWith(
|
||||
matches((updateTrustModel: UpdateDevicesTrustRequest) => {
|
||||
return (
|
||||
updateTrustModel.currentDevice.encryptedPublicKey ===
|
||||
"2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj" &&
|
||||
updateTrustModel.currentDevice.encryptedUserKey === "4.ZW5jcnlwdGVkdXNlcg=="
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { TrustedDeviceKeysRequest } from "../../services/devices/requests/trusted-device-keys.request";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
|
||||
export class DevicesApiServiceImplementation implements DevicesApiServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/devices/knowndevice",
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
(headers) => {
|
||||
headers.set("X-Device-Identifier", deviceIdentifier);
|
||||
headers.set("X-Request-Email", Utils.fromUtf8ToUrlB64(email));
|
||||
}
|
||||
);
|
||||
return r as boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device by identifier
|
||||
* @param deviceIdentifier - client generated id (not device id in DB)
|
||||
*/
|
||||
async getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
`/devices/identifier/${deviceIdentifier}`,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new DeviceResponse(r);
|
||||
}
|
||||
|
||||
async getDevices(): Promise<ListResponse<DeviceResponse>> {
|
||||
const r = await this.apiService.send("GET", "/devices", null, true, true, null);
|
||||
return new ListResponse(r, DeviceResponse);
|
||||
}
|
||||
|
||||
async updateTrustedDeviceKeys(
|
||||
deviceIdentifier: string,
|
||||
devicePublicKeyEncryptedUserKey: string,
|
||||
userKeyEncryptedDevicePublicKey: string,
|
||||
deviceKeyEncryptedDevicePrivateKey: string
|
||||
): Promise<DeviceResponse> {
|
||||
const request = new TrustedDeviceKeysRequest(
|
||||
devicePublicKeyEncryptedUserKey,
|
||||
userKeyEncryptedDevicePublicKey,
|
||||
deviceKeyEncryptedDevicePrivateKey
|
||||
);
|
||||
|
||||
const result = await this.apiService.send(
|
||||
"PUT",
|
||||
`/devices/${deviceIdentifier}/keys`,
|
||||
request,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
return new DeviceResponse(result);
|
||||
}
|
||||
|
||||
async updateTrust(updateDevicesTrustRequestModel: UpdateDevicesTrustRequest): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"/devices/update-trust",
|
||||
updateDevicesTrustRequestModel,
|
||||
true,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async getDeviceKeys(
|
||||
deviceIdentifier: string,
|
||||
secretVerificationRequest: SecretVerificationRequest
|
||||
): Promise<ProtectedDeviceResponse> {
|
||||
const result = await this.apiService.send(
|
||||
"POST",
|
||||
`/devices/${deviceIdentifier}/retrieve-keys`,
|
||||
secretVerificationRequest,
|
||||
true,
|
||||
true
|
||||
);
|
||||
return new ProtectedDeviceResponse(result);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { MasterKey, SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||
import { TokenService } from "../abstractions/token.service";
|
||||
import { KdfConfig } from "../models/domain/kdf-config";
|
||||
@@ -45,8 +45,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
|
||||
async migrateUser() {
|
||||
const organization = await this.getManagingOrganization();
|
||||
const key = await this.cryptoService.getKey();
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
|
||||
const masterKey = await this.cryptoService.getMasterKey();
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
|
||||
try {
|
||||
await this.apiService.postUserKeyToKeyConnector(
|
||||
@@ -60,12 +60,13 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
await this.apiService.postConvertToKeyConnector();
|
||||
}
|
||||
|
||||
async getAndSetKey(url: string) {
|
||||
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
||||
async setMasterKeyFromUrl(url: string) {
|
||||
try {
|
||||
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
|
||||
const k = new SymmetricCryptoKey(keyArr);
|
||||
await this.cryptoService.setKey(k);
|
||||
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
}
|
||||
@@ -87,17 +88,18 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
||||
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
|
||||
|
||||
const k = await this.cryptoService.makeKey(
|
||||
const masterKey = await this.cryptoService.makeMasterKey(
|
||||
Utils.fromBufferToB64(password),
|
||||
await this.tokenService.getEmail(),
|
||||
kdf,
|
||||
kdfConfig
|
||||
);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
|
||||
await this.cryptoService.setKey(k);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
await this.cryptoService.setMasterKey(masterKey);
|
||||
|
||||
const encKey = await this.cryptoService.makeEncKey(k);
|
||||
await this.cryptoService.setEncKey(encKey[1].encryptedString);
|
||||
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey[0]);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
|
||||
|
||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
||||
|
||||
@@ -109,7 +111,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
|
||||
const keys = new KeysRequest(pubKey, privKey.encryptedString);
|
||||
const setPasswordRequest = new SetKeyConnectorKeyRequest(
|
||||
encKey[1].encryptedString,
|
||||
userKey[1].encryptedString,
|
||||
kdf,
|
||||
kdfConfig,
|
||||
orgId,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
||||
|
||||
describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let service: PasswordResetEnrollmentServiceImplementation;
|
||||
|
||||
beforeEach(() => {
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
stateService = mock<StateService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
organizationUserService = mock<OrganizationUserService>();
|
||||
i18nService = mock<I18nService>();
|
||||
service = new PasswordResetEnrollmentServiceImplementation(
|
||||
organizationApiService,
|
||||
stateService,
|
||||
cryptoService,
|
||||
organizationUserService,
|
||||
i18nService
|
||||
);
|
||||
});
|
||||
|
||||
describe("enrollIfRequired", () => {
|
||||
it("should not enroll when user is already enrolled in password reset", async () => {
|
||||
const mockResponse = new OrganizationAutoEnrollStatusResponse({
|
||||
ResetPasswordEnabled: true,
|
||||
Id: "orgId",
|
||||
});
|
||||
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
|
||||
|
||||
const enrollSpy = jest.spyOn(service, "enroll");
|
||||
enrollSpy.mockResolvedValue();
|
||||
|
||||
await service.enrollIfRequired("ssoId");
|
||||
|
||||
expect(service.enroll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should enroll when user is not enrolled in password reset", async () => {
|
||||
const mockResponse = new OrganizationAutoEnrollStatusResponse({
|
||||
ResetPasswordEnabled: false,
|
||||
Id: "orgId",
|
||||
});
|
||||
organizationApiService.getAutoEnrollStatus.mockResolvedValue(mockResponse);
|
||||
|
||||
const enrollSpy = jest.spyOn(service, "enroll");
|
||||
enrollSpy.mockResolvedValue();
|
||||
|
||||
await service.enrollIfRequired("ssoId");
|
||||
|
||||
expect(service.enroll).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("enroll", () => {
|
||||
it("should throw an error if the organization keys are not found", async () => {
|
||||
organizationApiService.getKeys.mockResolvedValue(null);
|
||||
i18nService.t.mockReturnValue("resetPasswordOrgKeysError");
|
||||
|
||||
const result = () => service.enroll("orgId");
|
||||
|
||||
await expect(result).rejects.toThrowError("resetPasswordOrgKeysError");
|
||||
});
|
||||
|
||||
it("should enroll the user when no user id or key is provided", async () => {
|
||||
const orgKeyResponse = {
|
||||
publicKey: "publicKey",
|
||||
privateKey: "privateKey",
|
||||
};
|
||||
const encryptedKey = { encryptedString: "encryptedString" };
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||
stateService.getUserId.mockResolvedValue("userId");
|
||||
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
|
||||
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId");
|
||||
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment
|
||||
).toHaveBeenCalledWith(
|
||||
"orgId",
|
||||
"userId",
|
||||
expect.objectContaining({
|
||||
resetPasswordKey: encryptedKey.encryptedString,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should enroll the user when a user id and key is provided", async () => {
|
||||
const orgKeyResponse = {
|
||||
publicKey: "publicKey",
|
||||
privateKey: "privateKey",
|
||||
};
|
||||
const encryptedKey = { encryptedString: "encryptedString" };
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId", "userId", { key: "key" } as any);
|
||||
|
||||
expect(
|
||||
organizationUserService.putOrganizationUserResetPasswordEnrollment
|
||||
).toHaveBeenCalledWith(
|
||||
"orgId",
|
||||
"userId",
|
||||
expect.objectContaining({
|
||||
resetPasswordKey: encryptedKey.encryptedString,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { OrganizationUserService } from "../../abstractions/organization-user/organization-user.service";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../abstractions/organization-user/requests";
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { UserKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
|
||||
|
||||
export class PasswordResetEnrollmentServiceImplementation
|
||||
implements PasswordResetEnrollmentServiceAbstraction
|
||||
{
|
||||
constructor(
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected stateService: StateService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected organizationUserService: OrganizationUserService,
|
||||
protected i18nService: I18nService
|
||||
) {}
|
||||
|
||||
async enrollIfRequired(organizationSsoIdentifier: string): Promise<void> {
|
||||
const orgAutoEnrollStatusResponse = await this.organizationApiService.getAutoEnrollStatus(
|
||||
organizationSsoIdentifier
|
||||
);
|
||||
|
||||
if (!orgAutoEnrollStatusResponse.resetPasswordEnabled) {
|
||||
await this.enroll(orgAutoEnrollStatusResponse.id, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
async enroll(organizationId: string): Promise<void>;
|
||||
async enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
|
||||
async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise<void> {
|
||||
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
|
||||
if (orgKeyResponse == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
|
||||
|
||||
userId = userId ?? (await this.stateService.getUserId());
|
||||
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
|
||||
// RSA Encrypt user's userKey.key with organization public key
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||
|
||||
await this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
|
||||
organizationId,
|
||||
userId,
|
||||
resetRequest
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { Verification } from "../../../types/verification";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -13,6 +14,7 @@ import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
|
||||
*/
|
||||
export class UserVerificationService implements UserVerificationServiceAbstraction {
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private cryptoService: CryptoService,
|
||||
private i18nService: I18nService,
|
||||
private userVerificationApiService: UserVerificationApiServiceAbstraction
|
||||
@@ -37,9 +39,18 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||
if (verification.type === VerificationType.OTP) {
|
||||
request.otp = verification.secret;
|
||||
} else {
|
||||
let masterKey = await this.cryptoService.getMasterKey();
|
||||
if (!masterKey && !alreadyHashed) {
|
||||
masterKey = await this.cryptoService.makeMasterKey(
|
||||
verification.secret,
|
||||
await this.stateService.getEmail(),
|
||||
await this.stateService.getKdfType(),
|
||||
await this.stateService.getKdfConfig()
|
||||
);
|
||||
}
|
||||
request.masterPasswordHash = alreadyHashed
|
||||
? verification.secret
|
||||
: await this.cryptoService.hashPassword(verification.secret, null);
|
||||
: await this.cryptoService.hashMasterKey(verification.secret, masterKey);
|
||||
}
|
||||
|
||||
return request;
|
||||
@@ -61,13 +72,23 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||
throw new Error(this.i18nService.t("invalidVerificationCode"));
|
||||
}
|
||||
} else {
|
||||
let masterKey = await this.cryptoService.getMasterKey();
|
||||
if (!masterKey) {
|
||||
masterKey = await this.cryptoService.makeMasterKey(
|
||||
verification.secret,
|
||||
await this.stateService.getEmail(),
|
||||
await this.stateService.getKdfType(),
|
||||
await this.stateService.getKdfConfig()
|
||||
);
|
||||
}
|
||||
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
|
||||
verification.secret,
|
||||
null
|
||||
masterKey
|
||||
);
|
||||
if (!passwordValid) {
|
||||
throw new Error(this.i18nService.t("invalidMasterPassword"));
|
||||
}
|
||||
this.cryptoService.setMasterKey(masterKey);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -76,6 +97,30 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||
await this.userVerificationApiService.postAccountRequestOTP();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has master password or can only use passwordless technologies to log in
|
||||
* Note: This only checks the server, not the local state
|
||||
* @param userId The user id to check. If not provided, the current user is used
|
||||
* @returns True if the user has a master password
|
||||
*/
|
||||
async hasMasterPassword(userId?: string): Promise<boolean> {
|
||||
const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId });
|
||||
|
||||
if (decryptionOptions?.hasMasterPassword != undefined) {
|
||||
return decryptionOptions.hasMasterPassword;
|
||||
}
|
||||
|
||||
// TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0
|
||||
return !(await this.stateService.getUsesKeyConnector({ userId }));
|
||||
}
|
||||
|
||||
async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean> {
|
||||
return (
|
||||
(await this.hasMasterPassword(userId)) &&
|
||||
(await this.cryptoService.getMasterKeyHash()) != null
|
||||
);
|
||||
}
|
||||
|
||||
private validateInput(verification: Verification) {
|
||||
if (verification?.secret == null || verification.secret === "") {
|
||||
if (verification.type === VerificationType.OTP) {
|
||||
|
||||
@@ -23,3 +23,16 @@ export enum DeviceType {
|
||||
SDK = 21,
|
||||
Server = 22,
|
||||
}
|
||||
|
||||
export const MobileDeviceTypes: Set<DeviceType> = new Set([
|
||||
DeviceType.Android,
|
||||
DeviceType.iOS,
|
||||
DeviceType.AndroidAmazon,
|
||||
]);
|
||||
|
||||
export const DesktopDeviceTypes: Set<DeviceType> = new Set([
|
||||
DeviceType.WindowsDesktop,
|
||||
DeviceType.MacOsDesktop,
|
||||
DeviceType.LinuxDesktop,
|
||||
DeviceType.UWP,
|
||||
]);
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum EventType {
|
||||
User_ClientExportedVault = 1007,
|
||||
User_UpdatedTempPassword = 1008,
|
||||
User_MigratedKeyToKeyConnector = 1009,
|
||||
User_RequestedDeviceApproval = 1010,
|
||||
|
||||
Cipher_Created = 1100,
|
||||
Cipher_Updated = 1101,
|
||||
@@ -51,6 +52,8 @@ export enum EventType {
|
||||
OrganizationUser_FirstSsoLogin = 1510,
|
||||
OrganizationUser_Revoked = 1511,
|
||||
OrganizationUser_Restored = 1512,
|
||||
OrganizationUser_ApprovedAuthRequest = 1513,
|
||||
OrganizationUser_RejectedAuthRequest = 1514,
|
||||
|
||||
Organization_Updated = 1600,
|
||||
Organization_PurgedVault = 1601,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum KeySuffixOptions {
|
||||
Auto = "auto",
|
||||
Biometric = "biometric",
|
||||
Pin = "pin",
|
||||
}
|
||||
|
||||
@@ -5,82 +5,420 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
import { KeySuffixOptions, KdfType, HashPurpose } from "../../enums";
|
||||
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
MasterKey,
|
||||
OrgKey,
|
||||
PinKey,
|
||||
ProviderKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class CryptoService {
|
||||
setKey: (key: SymmetricCryptoKey) => Promise<any>;
|
||||
setKeyHash: (keyHash: string) => Promise<void>;
|
||||
setEncKey: (encKey: string) => Promise<void>;
|
||||
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
|
||||
/**
|
||||
* Sets the provided user key and stores
|
||||
* any other necessary versions (such as auto, biometrics,
|
||||
* or pin)
|
||||
* @param key The user key to set
|
||||
* @param userId The desired user
|
||||
*/
|
||||
setUserKey: (key: UserKey, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Gets the user key from memory and sets it again,
|
||||
* kicking off a refresh of any additional keys
|
||||
* (such as auto, biometrics, or pin)
|
||||
*/
|
||||
/**
|
||||
* Check if the current sessions has ever had a user key, i.e. has ever been unlocked/decrypted.
|
||||
* This is key for differentiating between TDE locked and standard locked states.
|
||||
* @param userId The desired user
|
||||
* @returns True if the current session has ever had a user key
|
||||
*/
|
||||
getEverHadUserKey: (userId?: string) => Promise<boolean>;
|
||||
refreshAdditionalKeys: () => Promise<void>;
|
||||
/**
|
||||
* Retrieves the user key
|
||||
* @param userId The desired user
|
||||
* @returns The user key
|
||||
*/
|
||||
getUserKey: (userId?: string) => Promise<UserKey>;
|
||||
/**
|
||||
* Use for encryption/decryption of data in order to support legacy
|
||||
* encryption models. It will return the user key if available,
|
||||
* if not it will return the master key.
|
||||
* @param userId The desired user
|
||||
*/
|
||||
getUserKeyWithLegacySupport: (userId?: string) => Promise<UserKey>;
|
||||
/**
|
||||
* Retrieves the user key from storage
|
||||
* @param keySuffix The desired version of the user's key to retrieve
|
||||
* @param userId The desired user
|
||||
* @returns The user key
|
||||
*/
|
||||
getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<UserKey>;
|
||||
|
||||
/**
|
||||
* @returns True if the user key is available
|
||||
*/
|
||||
hasUserKey: () => Promise<boolean>;
|
||||
/**
|
||||
* @param userId The desired user
|
||||
* @returns True if the user key is set in memory
|
||||
*/
|
||||
hasUserKeyInMemory: (userId?: string) => Promise<boolean>;
|
||||
/**
|
||||
* @param keySuffix The desired version of the user's key to check
|
||||
* @param userId The desired user
|
||||
* @returns True if the provided version of the user key is stored
|
||||
*/
|
||||
hasUserKeyStored: (keySuffix: KeySuffixOptions, userId?: string) => Promise<boolean>;
|
||||
/**
|
||||
* Generates a new user key
|
||||
* @param masterKey The user's master key
|
||||
* @returns A new user key and the master key protected version of it
|
||||
*/
|
||||
makeUserKey: (key: MasterKey) => Promise<[UserKey, EncString]>;
|
||||
/**
|
||||
* Clears the user key
|
||||
* @param clearStoredKeys Clears all stored versions of the user keys as well,
|
||||
* such as the biometrics key
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearUserKey: (clearSecretStorage?: boolean, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Clears the user's stored version of the user key
|
||||
* @param keySuffix The desired version of the key to clear
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearStoredUserKey: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Stores the master key encrypted user key
|
||||
* @param userKeyMasterKey The master key encrypted user key to set
|
||||
* @param userId The desired user
|
||||
*/
|
||||
setMasterKeyEncryptedUserKey: (UserKeyMasterKey: string, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Sets the user's master key
|
||||
* @param key The user's master key to set
|
||||
* @param userId The desired user
|
||||
*/
|
||||
setMasterKey: (key: MasterKey, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* @param userId The desired user
|
||||
* @returns The user's master key
|
||||
*/
|
||||
getMasterKey: (userId?: string) => Promise<MasterKey>;
|
||||
|
||||
/**
|
||||
* @param password The user's master password that will be used to derive a master key if one isn't found
|
||||
* @param userId The desired user
|
||||
*/
|
||||
getOrDeriveMasterKey: (password: string, userId?: string) => Promise<MasterKey>;
|
||||
/**
|
||||
* Generates a master key from the provided password
|
||||
* @param password The user's master password
|
||||
* @param email The user's email
|
||||
* @param kdf The user's selected key derivation function to use
|
||||
* @param KdfConfig The user's key derivation function configuration
|
||||
* @returns A master key derived from the provided password
|
||||
*/
|
||||
makeMasterKey: (
|
||||
password: string,
|
||||
email: string,
|
||||
kdf: KdfType,
|
||||
KdfConfig: KdfConfig
|
||||
) => Promise<MasterKey>;
|
||||
/**
|
||||
* Clears the user's master key
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearMasterKey: (userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Encrypts the existing (or provided) user key with the
|
||||
* provided master key
|
||||
* @param masterKey The user's master key
|
||||
* @param userKey The user key
|
||||
* @returns The user key and the master key protected version of it
|
||||
*/
|
||||
encryptUserKeyWithMasterKey: (
|
||||
masterKey: MasterKey,
|
||||
userKey?: UserKey
|
||||
) => Promise<[UserKey, EncString]>;
|
||||
/**
|
||||
* Decrypts the user key with the provided master key
|
||||
* @param masterKey The user's master key
|
||||
* @param userKey The user's encrypted symmetric key
|
||||
* @param userId The desired user
|
||||
* @returns The user key
|
||||
*/
|
||||
decryptUserKeyWithMasterKey: (
|
||||
masterKey: MasterKey,
|
||||
userKey?: EncString,
|
||||
userId?: string
|
||||
) => Promise<UserKey>;
|
||||
/**
|
||||
* Creates a master password hash from the user's master password. Can
|
||||
* be used for local authentication or for server authentication depending
|
||||
* on the hashPurpose provided.
|
||||
* @param password The user's master password
|
||||
* @param key The user's master key
|
||||
* @param hashPurpose The iterations to use for the hash
|
||||
* @returns The user's master password hash
|
||||
*/
|
||||
hashMasterKey: (password: string, key: MasterKey, hashPurpose?: HashPurpose) => Promise<string>;
|
||||
/**
|
||||
* Sets the user's master password hash
|
||||
* @param keyHash The user's master password hash to set
|
||||
*/
|
||||
setMasterKeyHash: (keyHash: string) => Promise<void>;
|
||||
/**
|
||||
* @returns The user's master password hash
|
||||
*/
|
||||
getMasterKeyHash: () => Promise<string>;
|
||||
/**
|
||||
* Clears the user's stored master password hash
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearMasterKeyHash: (userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Compares the provided master password to the stored password hash and server password hash.
|
||||
* Updates the stored hash if outdated.
|
||||
* @param masterPassword The user's master password
|
||||
* @param key The user's master key
|
||||
* @returns True if the provided master password matches either the stored
|
||||
* key hash or the server key hash
|
||||
*/
|
||||
compareAndUpdateKeyHash: (masterPassword: string, masterKey: MasterKey) => Promise<boolean>;
|
||||
/**
|
||||
* Stores the encrypted organization keys and clears any decrypted
|
||||
* organization keys currently in memory
|
||||
* @param orgs The organizations to set keys for
|
||||
* @param providerOrgs The provider organizations to set keys for
|
||||
*/
|
||||
setOrgKeys: (
|
||||
orgs: ProfileOrganizationResponse[],
|
||||
providerOrgs: ProfileProviderOrganizationResponse[]
|
||||
) => Promise<void>;
|
||||
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
|
||||
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyHash: () => Promise<string>;
|
||||
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||
getPublicKey: () => Promise<Uint8Array>;
|
||||
getPrivateKey: () => Promise<Uint8Array>;
|
||||
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
|
||||
/**
|
||||
* Returns the organization's symmetric key
|
||||
* @param orgId The desired organization
|
||||
* @returns The organization's symmetric key
|
||||
*/
|
||||
getOrgKey: (orgId: string) => Promise<OrgKey>;
|
||||
/**
|
||||
* @returns A map of the organization Ids to their symmetric keys
|
||||
*/
|
||||
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
|
||||
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
|
||||
getKeyForUserEncryption: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
|
||||
hasKey: () => Promise<boolean>;
|
||||
hasKeyInMemory: (userId?: string) => Promise<boolean>;
|
||||
hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<boolean>;
|
||||
hasEncKey: () => Promise<boolean>;
|
||||
clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise<any>;
|
||||
clearKeyHash: () => Promise<any>;
|
||||
clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<any>;
|
||||
clearProviderKeys: (memoryOnly?: boolean) => Promise<any>;
|
||||
clearPinProtectedKey: () => Promise<any>;
|
||||
clearKeys: (userId?: string) => Promise<any>;
|
||||
toggleKey: () => Promise<any>;
|
||||
makeKey: (
|
||||
password: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfConfig: KdfConfig
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeKeyFromPin: (
|
||||
/**
|
||||
* Uses the org key to derive a new symmetric key for encrypting data
|
||||
* @param orgKey The organization's symmetric key
|
||||
*/
|
||||
makeDataEncKey: <T extends UserKey | OrgKey>(key: T) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
/**
|
||||
* Clears the user's stored organization keys
|
||||
* @param memoryOnly Clear only the in-memory keys
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Stores the encrypted provider keys and clears any decrypted
|
||||
* provider keys currently in memory
|
||||
* @param providers The providers to set keys for
|
||||
*/
|
||||
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
|
||||
/**
|
||||
* @param providerId The desired provider
|
||||
* @returns The provider's symmetric key
|
||||
*/
|
||||
getProviderKey: (providerId: string) => Promise<ProviderKey>;
|
||||
/**
|
||||
* @returns A map of the provider Ids to their symmetric keys
|
||||
*/
|
||||
getProviderKeys: () => Promise<Map<string, ProviderKey>>;
|
||||
/**
|
||||
* @param memoryOnly Clear only the in-memory keys
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearProviderKeys: (memoryOnly?: boolean, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* Returns the public key from memory. If not available, extracts it
|
||||
* from the private key and stores it in memory
|
||||
* @returns The user's public key
|
||||
*/
|
||||
getPublicKey: () => Promise<Uint8Array>;
|
||||
/**
|
||||
* Creates a new organization key and encrypts it with the user's public key.
|
||||
* This method can also return Provider keys for creating new Provider users.
|
||||
* @returns The new encrypted org key and the decrypted key itself
|
||||
*/
|
||||
makeOrgKey: <T extends OrgKey | ProviderKey>() => Promise<[EncString, T]>;
|
||||
/**
|
||||
* Sets the the user's encrypted private key in storage and
|
||||
* clears the decrypted private key from memory
|
||||
* Note: does not clear the private key if null is provided
|
||||
* @param encPrivateKey An encrypted private key
|
||||
*/
|
||||
setPrivateKey: (encPrivateKey: string) => Promise<void>;
|
||||
/**
|
||||
* Returns the private key from memory. If not available, decrypts it
|
||||
* from storage and stores it in memory
|
||||
* @returns The user's private key
|
||||
*/
|
||||
getPrivateKey: () => Promise<Uint8Array>;
|
||||
/**
|
||||
* Generates a fingerprint phrase for the user based on their public key
|
||||
* @param fingerprintMaterial Fingerprint material
|
||||
* @param publicKey The user's public key
|
||||
* @returns The user's fingerprint phrase
|
||||
*/
|
||||
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
|
||||
/**
|
||||
* Generates a new keypair
|
||||
* @param key A key to encrypt the private key with. If not provided,
|
||||
* defaults to the user key
|
||||
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
|
||||
*/
|
||||
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
|
||||
/**
|
||||
* Clears the user's key pair
|
||||
* @param memoryOnly Clear only the in-memory keys
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<void[]>;
|
||||
/**
|
||||
* @param pin The user's pin
|
||||
* @param salt The user's salt
|
||||
* @param kdf The user's kdf
|
||||
* @param kdfConfig The user's kdf config
|
||||
* @returns A key derived from the user's pin
|
||||
*/
|
||||
makePinKey: (pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig) => Promise<PinKey>;
|
||||
/**
|
||||
* Clears the user's pin keys from storage
|
||||
* Note: This will remove the stored pin and as a result,
|
||||
* disable pin protection for the user
|
||||
* @param userId The desired user
|
||||
*/
|
||||
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 kdf The user's KDF
|
||||
* @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
|
||||
*/
|
||||
decryptUserKeyWithPin: (
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfConfig: KdfConfig,
|
||||
protectedKeyCs?: EncString
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
|
||||
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
|
||||
makePinKey: (
|
||||
) => 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 kdf User's KdfType
|
||||
* @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
|
||||
*/
|
||||
decryptAndMigrateOldPinKey: (
|
||||
masterPasswordOnRestart: boolean,
|
||||
pin: string,
|
||||
email: string,
|
||||
kdf: KdfType,
|
||||
kdfConfig: KdfConfig,
|
||||
oldPinKey: EncString
|
||||
) => Promise<UserKey>;
|
||||
/**
|
||||
* Replaces old master auto keys with new user auto keys
|
||||
*/
|
||||
migrateAutoKeyIfNeeded: (userId?: string) => Promise<void>;
|
||||
/**
|
||||
* @param keyMaterial The key material to derive the send key from
|
||||
* @returns A new send key
|
||||
*/
|
||||
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
|
||||
/**
|
||||
* Clears all of the user's keys from storage
|
||||
* @param userId The user's Id
|
||||
*/
|
||||
clearKeys: (userId?: string) => Promise<any>;
|
||||
/**
|
||||
* RSA encrypts a value.
|
||||
* @param data The data to encrypt
|
||||
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
|
||||
* @returns The encrypted data
|
||||
*/
|
||||
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
|
||||
/**
|
||||
* Decrypts a value using RSA.
|
||||
* @param encValue The encrypted value to decrypt
|
||||
* @param privateKeyValue The private key to use for decryption
|
||||
* @returns The decrypted value
|
||||
*/
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
|
||||
randomNumber: (min: number, max: number) => Promise<number>;
|
||||
|
||||
/**
|
||||
* Initialize all necessary crypto keys needed for a new account.
|
||||
* Warning! This completely replaces any existing keys!
|
||||
* @returns The user's newly created public key, private key, and encrypted private key
|
||||
*/
|
||||
initAccount: () => Promise<{
|
||||
userKey: UserKey;
|
||||
publicKey: string;
|
||||
privateKey: EncString;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
|
||||
*/
|
||||
decryptMasterKeyWithPin: (
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdf: KdfType,
|
||||
kdfConfig: KdfConfig
|
||||
) => Promise<SymmetricCryptoKey>;
|
||||
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
|
||||
hashPassword: (
|
||||
password: string,
|
||||
key: SymmetricCryptoKey,
|
||||
hashPurpose?: HashPurpose
|
||||
) => Promise<string>;
|
||||
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
remakeEncKey: (
|
||||
key: SymmetricCryptoKey,
|
||||
encKey?: SymmetricCryptoKey
|
||||
) => Promise<[SymmetricCryptoKey, EncString]>;
|
||||
kdfConfig: KdfConfig,
|
||||
protectedKeyCs?: EncString
|
||||
) => Promise<MasterKey>;
|
||||
/**
|
||||
* Previously, the master key was used for any additional key like the biometrics or pin key.
|
||||
* We have switched to using the user key for these purposes. This method is for clearing the state
|
||||
* of the older keys on logout or post migration.
|
||||
* @param keySuffix The desired type of key to clear
|
||||
* @param userId The desired user
|
||||
*/
|
||||
clearDeprecatedKeys: (keySuffix: KeySuffixOptions, userId?: string) => Promise<void>;
|
||||
/**
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.encrypt
|
||||
*/
|
||||
encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise<EncString>;
|
||||
/**
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.encryptToBytes
|
||||
*/
|
||||
encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
|
||||
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
|
||||
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.decryptToBytes
|
||||
*/
|
||||
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.decryptToUtf8
|
||||
*/
|
||||
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
|
||||
/**
|
||||
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
|
||||
* and then call encryptService.decryptToBytes
|
||||
*/
|
||||
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<Uint8Array>;
|
||||
randomNumber: (min: number, max: number) => Promise<number>;
|
||||
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { OrganizationData } from "../../admin-console/models/data/organization.d
|
||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
||||
import { Policy } from "../../admin-console/models/domain/policy";
|
||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
|
||||
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
@@ -23,10 +24,19 @@ import { CipherView } from "../../vault/models/view/cipher.view";
|
||||
import { CollectionView } from "../../vault/models/view/collection.view";
|
||||
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
|
||||
import { ServerConfigData } from "../models/data/server-config.data";
|
||||
import { Account, AccountSettingsSettings } from "../models/domain/account";
|
||||
import {
|
||||
Account,
|
||||
AccountDecryptionOptions,
|
||||
AccountSettingsSettings,
|
||||
} from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
DeviceKey,
|
||||
MasterKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
accounts$: Observable<{ [userId: string]: T }>;
|
||||
@@ -71,24 +81,106 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise<void>;
|
||||
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
|
||||
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* gets the user key
|
||||
*/
|
||||
getUserKey: (options?: StorageOptions) => Promise<UserKey>;
|
||||
/**
|
||||
* Sets the user key
|
||||
*/
|
||||
setUserKey: (value: UserKey, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* Gets the user's master key
|
||||
*/
|
||||
getMasterKey: (options?: StorageOptions) => Promise<MasterKey>;
|
||||
/**
|
||||
* Sets the user's master key
|
||||
*/
|
||||
setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* Gets the user key encrypted by the master key
|
||||
*/
|
||||
getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* Sets the user key encrypted by the master key
|
||||
*/
|
||||
setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* Gets the user's auto key
|
||||
*/
|
||||
getUserKeyAutoUnlock: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* Sets the user's auto key
|
||||
*/
|
||||
setUserKeyAutoUnlock: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* Gets the user's biometric key
|
||||
*/
|
||||
getUserKeyBiometric: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* Checks if the user has a biometric key available
|
||||
*/
|
||||
hasUserKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||
/**
|
||||
* Sets the user's biometric key
|
||||
*/
|
||||
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* Gets the user key encrypted by the Pin key.
|
||||
* Used when Lock with MP on Restart is disabled
|
||||
*/
|
||||
getPinKeyEncryptedUserKey: (options?: StorageOptions) => Promise<EncString>;
|
||||
/**
|
||||
* Sets the user key encrypted by the Pin key.
|
||||
* Used when Lock with MP on Restart is disabled
|
||||
*/
|
||||
setPinKeyEncryptedUserKey: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* Gets the ephemeral version of the user key encrypted by the Pin key.
|
||||
* Used when Lock with MP on Restart is enabled
|
||||
*/
|
||||
getPinKeyEncryptedUserKeyEphemeral: (options?: StorageOptions) => Promise<EncString>;
|
||||
/**
|
||||
* Sets the ephemeral version of the user key encrypted by the Pin key.
|
||||
* Used when Lock with MP on Restart is enabled
|
||||
*/
|
||||
setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use getUserKeyMasterKey instead
|
||||
*/
|
||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use setUserKeyMasterKey instead
|
||||
*/
|
||||
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated For legacy purposes only, use getMasterKey instead
|
||||
*/
|
||||
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use getUserKeyAuto instead
|
||||
*/
|
||||
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use setUserKeyAuto instead
|
||||
*/
|
||||
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getCryptoMasterKeyB64: (options?: StorageOptions) => Promise<string>;
|
||||
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use getUserKeyBiometric instead
|
||||
*/
|
||||
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use hasUserKeyBiometric instead
|
||||
*/
|
||||
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use setUserKeyBiometric instead
|
||||
*/
|
||||
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
|
||||
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
|
||||
setDecryptedCollections: (value: CollectionView[], options?: StorageOptions) => Promise<void>;
|
||||
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
|
||||
setDecryptedCryptoSymmetricKey: (
|
||||
value: SymmetricCryptoKey,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getDecryptedOrganizationKeys: (
|
||||
options?: StorageOptions
|
||||
) => Promise<Map<string, SymmetricCryptoKey>>;
|
||||
@@ -103,7 +195,13 @@ export abstract class StateService<T extends Account = Account> {
|
||||
value: GeneratedPasswordHistory[],
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use getDecryptedUserKeyPin instead
|
||||
*/
|
||||
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
|
||||
*/
|
||||
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this, use PolicyService
|
||||
@@ -164,7 +262,21 @@ export abstract class StateService<T extends Account = Account> {
|
||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
|
||||
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
|
||||
setDeviceKey: (value: DeviceKey | null, options?: StorageOptions) => Promise<void>;
|
||||
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
|
||||
setAdminAuthRequest: (
|
||||
adminAuthRequest: AdminAuthRequestStorable,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getShouldTrustDevice: (options?: StorageOptions) => Promise<boolean | null>;
|
||||
setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getAccountDecryptionOptions: (
|
||||
options?: StorageOptions
|
||||
) => Promise<AccountDecryptionOptions | null>;
|
||||
setAccountDecryptionOptions: (
|
||||
value: AccountDecryptionOptions,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
@@ -205,8 +317,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
value: { [id: string]: CollectionData },
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
|
||||
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use FolderService
|
||||
*/
|
||||
@@ -232,7 +342,13 @@ export abstract class StateService<T extends Account = Account> {
|
||||
value: GeneratedPasswordHistory[],
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use getEncryptedUserKeyPin instead
|
||||
*/
|
||||
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
|
||||
/**
|
||||
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
|
||||
*/
|
||||
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use PolicyService
|
||||
@@ -269,6 +385,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
||||
getEverHadUserKey: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEverHadUserKey: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getForcePasswordResetReason: (options?: StorageOptions) => Promise<ForceResetPasswordReason>;
|
||||
@@ -327,7 +445,13 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
|
||||
getGeneratorOptions: (options?: StorageOptions) => Promise<any>;
|
||||
setGeneratorOptions: (value: any, 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>;
|
||||
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
|
||||
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
|
||||
@@ -353,6 +477,11 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSsoState: (options?: StorageOptions) => Promise<string>;
|
||||
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getUserSsoOrganizationIdentifier: (options?: StorageOptions) => Promise<string>;
|
||||
setUserSsoOrganizationIdentifier: (
|
||||
value: string | null,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { CsprngArray } from "../../../types/csprng";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { AccountKeys, EncryptionPair } from "./account";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
describe("AccountKeys", () => {
|
||||
describe("toJSON", () => {
|
||||
@@ -22,6 +23,23 @@ describe("AccountKeys", () => {
|
||||
const json = JSON.stringify(keys);
|
||||
expect(json).toContain('"publicKey":"hello"');
|
||||
});
|
||||
|
||||
// As the accountKeys.toJSON doesn't really serialize the device key
|
||||
// this method just checks the persistence of the deviceKey
|
||||
it("should persist deviceKey", () => {
|
||||
// Arrange
|
||||
const accountKeys = new AccountKeys();
|
||||
const deviceKeyBytesLength = 64;
|
||||
accountKeys.deviceKey = new SymmetricCryptoKey(
|
||||
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
|
||||
) as DeviceKey;
|
||||
|
||||
// Act
|
||||
const serializedKeys = accountKeys.toJSON();
|
||||
|
||||
// Assert
|
||||
expect(serializedKeys.deviceKey).toEqual(accountKeys.deviceKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
@@ -57,5 +75,24 @@ describe("AccountKeys", () => {
|
||||
} as any);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should deserialize deviceKey", () => {
|
||||
// Arrange
|
||||
const expectedKeyB64 =
|
||||
"ZJNnhx9BbJeb2EAq1hlMjqt6GFsg9G/GzoFf6SbPKsaiMhKGDcbHcwcyEg56Lh8lfilpZz4SRM6UA7oFCg+lSg==";
|
||||
|
||||
const symmetricCryptoKeyFromJsonSpy = jest.spyOn(SymmetricCryptoKey, "fromJSON");
|
||||
|
||||
// Act
|
||||
const accountKeys = AccountKeys.fromJSON({
|
||||
deviceKey: {
|
||||
keyB64: expectedKeyB64,
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Assert
|
||||
expect(symmetricCryptoKeyFromJsonSpy).toHaveBeenCalled();
|
||||
expect(accountKeys.deviceKey.keyB64).toEqual(expectedKeyB64);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,12 @@ import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||
import { Policy } from "../../../admin-console/models/domain/policy";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
||||
import { EnvironmentUrls } from "../../../auth/models/domain/environment-urls";
|
||||
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
|
||||
import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option";
|
||||
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||
import { UserDecryptionOptionsResponse } from "../../../auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { KdfType, UriMatchType } from "../../../enums";
|
||||
import { EventData } from "../../../models/data/event.data";
|
||||
import { GeneratedPasswordHistory } from "../../../tools/generator/password";
|
||||
@@ -22,8 +26,8 @@ import { CollectionView } from "../../../vault/models/view/collection.view";
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||
|
||||
import { EncString } from "./enc-string";
|
||||
import { DeviceKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
import { EncryptedString, EncString } from "./enc-string";
|
||||
import { MasterKey, SymmetricCryptoKey, UserKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
encrypted?: TEncrypted;
|
||||
@@ -99,15 +103,10 @@ export class AccountData {
|
||||
}
|
||||
|
||||
export class AccountKeys {
|
||||
cryptoMasterKey?: SymmetricCryptoKey;
|
||||
cryptoMasterKeyAuto?: string;
|
||||
cryptoMasterKeyB64?: string;
|
||||
cryptoMasterKeyBiometric?: string;
|
||||
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
deviceKey?: DeviceKey;
|
||||
userKey?: UserKey;
|
||||
masterKey?: MasterKey;
|
||||
masterKeyEncryptedUserKey?: string;
|
||||
deviceKey?: ReturnType<SymmetricCryptoKey["toJSON"]>;
|
||||
organizationKeys?: EncryptionPair<
|
||||
{ [orgId: string]: EncryptedOrganizationKeyData },
|
||||
Record<string, SymmetricCryptoKey>
|
||||
@@ -123,6 +122,18 @@ export class AccountKeys {
|
||||
publicKey?: Uint8Array;
|
||||
apiKeyClientSecret?: string;
|
||||
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
cryptoMasterKey?: SymmetricCryptoKey;
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
cryptoMasterKeyAuto?: string;
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
cryptoMasterKeyBiometric?: string;
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
|
||||
string,
|
||||
SymmetricCryptoKey
|
||||
>();
|
||||
|
||||
toJSON() {
|
||||
return Utils.merge(this, {
|
||||
publicKey: Utils.fromBufferToByteString(this.publicKey),
|
||||
@@ -135,6 +146,9 @@ export class AccountKeys {
|
||||
}
|
||||
|
||||
return Object.assign(new AccountKeys(), {
|
||||
userKey: SymmetricCryptoKey.fromJSON(obj?.userKey),
|
||||
masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey),
|
||||
deviceKey: obj?.deviceKey,
|
||||
cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey),
|
||||
cryptoSymmetricKey: EncryptionPair.fromJSON(
|
||||
obj?.cryptoSymmetricKey,
|
||||
@@ -173,6 +187,7 @@ export class AccountProfile {
|
||||
emailVerified?: boolean;
|
||||
entityId?: string;
|
||||
entityType?: string;
|
||||
everHadUserKey?: boolean;
|
||||
everBeenUnlocked?: boolean;
|
||||
forcePasswordResetReason?: ForceResetPasswordReason;
|
||||
hasPremiumPersonally?: boolean;
|
||||
@@ -223,7 +238,8 @@ export class AccountSettings {
|
||||
passwordGenerationOptions?: any;
|
||||
usernameGenerationOptions?: any;
|
||||
generatorOptions?: any;
|
||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
pinKeyEncryptedUserKey?: EncryptedString;
|
||||
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
|
||||
protectedPin?: string;
|
||||
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
|
||||
vaultTimeout?: number;
|
||||
@@ -234,6 +250,10 @@ export class AccountSettings {
|
||||
activateAutoFillOnPageLoadFromPolicy?: boolean;
|
||||
region?: string;
|
||||
smOnboardingTasks?: Record<string, Record<string, boolean>>;
|
||||
trustDeviceChoiceForDecryption?: boolean;
|
||||
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||
if (obj == null) {
|
||||
@@ -269,12 +289,106 @@ export class AccountTokens {
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountDecryptionOptions {
|
||||
hasMasterPassword: boolean;
|
||||
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
|
||||
keyConnectorOption?: KeyConnectorUserDecryptionOption;
|
||||
|
||||
constructor(init?: Partial<AccountDecryptionOptions>) {
|
||||
if (init) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: these nice getters don't work because the Account object is not properly being deserialized out of
|
||||
// JSON (the Account static fromJSON method is not running) so these getters don't exist on the
|
||||
// account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on
|
||||
// get hasTrustedDeviceOption(): boolean {
|
||||
// return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined;
|
||||
// }
|
||||
|
||||
// get hasKeyConnectorOption(): boolean {
|
||||
// return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined;
|
||||
// }
|
||||
|
||||
static fromResponse(response: UserDecryptionOptionsResponse): AccountDecryptionOptions {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountDecryptionOptions = new AccountDecryptionOptions();
|
||||
accountDecryptionOptions.hasMasterPassword = response.hasMasterPassword;
|
||||
|
||||
if (response.trustedDeviceOption) {
|
||||
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
response.trustedDeviceOption.hasAdminApproval,
|
||||
response.trustedDeviceOption.hasLoginApprovingDevice,
|
||||
response.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
);
|
||||
}
|
||||
|
||||
if (response.keyConnectorOption) {
|
||||
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||
response.keyConnectorOption.keyConnectorUrl
|
||||
);
|
||||
}
|
||||
|
||||
return accountDecryptionOptions;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountDecryptionOptions>): AccountDecryptionOptions {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj);
|
||||
|
||||
if (obj.trustedDeviceOption) {
|
||||
accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption(
|
||||
obj.trustedDeviceOption.hasAdminApproval,
|
||||
obj.trustedDeviceOption.hasLoginApprovingDevice,
|
||||
obj.trustedDeviceOption.hasManageResetPasswordPermission
|
||||
);
|
||||
}
|
||||
|
||||
if (obj.keyConnectorOption) {
|
||||
accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(
|
||||
obj.keyConnectorOption.keyConnectorUrl
|
||||
);
|
||||
}
|
||||
|
||||
return accountDecryptionOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export class LoginState {
|
||||
ssoOrganizationIdentifier?: string;
|
||||
|
||||
constructor(init?: Partial<LoginState>) {
|
||||
if (init) {
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<LoginState>): LoginState {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const loginState = Object.assign(new LoginState(), obj);
|
||||
return loginState;
|
||||
}
|
||||
}
|
||||
|
||||
export class Account {
|
||||
data?: AccountData = new AccountData();
|
||||
keys?: AccountKeys = new AccountKeys();
|
||||
profile?: AccountProfile = new AccountProfile();
|
||||
settings?: AccountSettings = new AccountSettings();
|
||||
tokens?: AccountTokens = new AccountTokens();
|
||||
decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions();
|
||||
loginState?: LoginState = new LoginState();
|
||||
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
Object.assign(this, {
|
||||
@@ -298,6 +412,15 @@ export class Account {
|
||||
...new AccountTokens(),
|
||||
...init?.tokens,
|
||||
},
|
||||
decryptionOptions: {
|
||||
...new AccountDecryptionOptions(),
|
||||
...init?.decryptionOptions,
|
||||
},
|
||||
loginState: {
|
||||
...new LoginState(),
|
||||
...init?.loginState,
|
||||
},
|
||||
adminAuthRequest: init?.adminAuthRequest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,6 +434,9 @@ export class Account {
|
||||
profile: AccountProfile.fromJSON(json?.profile),
|
||||
settings: AccountSettings.fromJSON(json?.settings),
|
||||
tokens: AccountTokens.fromJSON(json?.tokens),
|
||||
decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions),
|
||||
loginState: LoginState.fromJSON(json?.loginState),
|
||||
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { EncryptionType } from "../../../enums";
|
||||
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
OrgKey,
|
||||
SymmetricCryptoKey,
|
||||
UserKey,
|
||||
} from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { ContainerService } from "../../services/container.service";
|
||||
|
||||
@@ -225,12 +229,12 @@ describe("EncString", () => {
|
||||
|
||||
await encString.decrypt(null, key);
|
||||
|
||||
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
|
||||
expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
|
||||
it("gets an organization key if required", async () => {
|
||||
const orgKey = mock<SymmetricCryptoKey>();
|
||||
const orgKey = mock<OrgKey>();
|
||||
|
||||
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
|
||||
|
||||
@@ -241,13 +245,13 @@ describe("EncString", () => {
|
||||
});
|
||||
|
||||
it("gets the user's decryption key if required", async () => {
|
||||
const userKey = mock<SymmetricCryptoKey>();
|
||||
const userKey = mock<UserKey>();
|
||||
|
||||
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
|
||||
cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
|
||||
|
||||
await encString.decrypt(null, null);
|
||||
|
||||
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalledWith();
|
||||
expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../enums";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
@@ -7,7 +7,7 @@ import { Encrypted } from "../../interfaces/encrypted";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncString implements Encrypted {
|
||||
encryptedString?: string;
|
||||
encryptedString?: EncryptedString;
|
||||
encryptionType?: EncryptionType;
|
||||
decryptedValue?: string;
|
||||
data?: string;
|
||||
@@ -53,14 +53,14 @@ export class EncString implements Encrypted {
|
||||
|
||||
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
|
||||
if (iv != null) {
|
||||
this.encryptedString = encType + "." + iv + "|" + data;
|
||||
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
|
||||
} else {
|
||||
this.encryptedString = encType + "." + data;
|
||||
this.encryptedString = (encType + "." + data) as EncryptedString;
|
||||
}
|
||||
|
||||
// mac
|
||||
if (mac != null) {
|
||||
this.encryptedString += "|" + mac;
|
||||
this.encryptedString = (this.encryptedString + "|" + mac) as EncryptedString;
|
||||
}
|
||||
|
||||
this.encryptionType = encType;
|
||||
@@ -70,7 +70,7 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
|
||||
private initFromEncryptedString(encryptedString: string) {
|
||||
this.encryptedString = encryptedString as string;
|
||||
this.encryptedString = encryptedString as EncryptedString;
|
||||
if (!this.encryptedString) {
|
||||
return;
|
||||
}
|
||||
@@ -162,6 +162,8 @@ export class EncString implements Encrypted {
|
||||
const cryptoService = Utils.getContainerService().getCryptoService();
|
||||
return orgId != null
|
||||
? await cryptoService.getOrgKey(orgId)
|
||||
: await cryptoService.getKeyForUserEncryption();
|
||||
: await cryptoService.getUserKeyWithLegacySupport();
|
||||
}
|
||||
}
|
||||
|
||||
export type EncryptedString = Opaque<string, "EncString">;
|
||||
|
||||
@@ -78,3 +78,8 @@ export class SymmetricCryptoKey {
|
||||
|
||||
// Setup all separate key types as opaque types
|
||||
export type DeviceKey = Opaque<SymmetricCryptoKey, "DeviceKey">;
|
||||
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
|
||||
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
|
||||
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
|
||||
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
|
||||
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;
|
||||
|
||||
@@ -36,16 +36,25 @@ export class ConfigService implements ConfigServiceAbstraction {
|
||||
try {
|
||||
const response = await this.configApiService.get();
|
||||
|
||||
if (response != null) {
|
||||
const data = new ServerConfigData(response);
|
||||
const serverConfig = new ServerConfig(data);
|
||||
this._serverConfig.next(serverConfig);
|
||||
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) {
|
||||
return serverConfig;
|
||||
}
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new ServerConfigData(response);
|
||||
const serverConfig = new ServerConfig(data);
|
||||
this._serverConfig.next(serverConfig);
|
||||
|
||||
const userAuthStatus = await this.authService.getAuthStatus();
|
||||
if (userAuthStatus !== AuthenticationStatus.LoggedOut) {
|
||||
// Store the config for offline use if the user is logged in
|
||||
await this.stateService.setServerConfig(data);
|
||||
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
|
||||
}
|
||||
// Always return new server config from server to calling method
|
||||
// to ensure up to date information
|
||||
// This change is specifically for the getFeatureFlag > buildServerConfig flow
|
||||
// for locked or logged in users.
|
||||
return serverConfig;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user