1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 01:33:33 +00:00

Conflict resolution

This commit is contained in:
Carlos Gonçalves
2024-04-02 17:18:45 +01:00
parent 8a1df6671a
commit 0c0c2039ed
699 changed files with 17230 additions and 8095 deletions

View File

@@ -18,12 +18,12 @@
{{ dialogOptions.bodyText | i18n }}
</p>
<app-callout
<bit-callout
*ngIf="dialogOptions.calloutOptions"
[type]="dialogOptions.calloutOptions.type"
>
{{ dialogOptions.calloutOptions.text | i18n }}
</app-callout>
</bit-callout>
</ng-container>
<!-- Shown when client side verification methods picked and no verification methods found -->

View File

@@ -12,6 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
DialogModule,
DialogService,
} from "@bitwarden/components";
@@ -34,6 +35,7 @@ import { UserVerificationFormInputComponent } from "./user-verification-form-inp
DialogModule,
AsyncActionsModule,
UserVerificationFormInputComponent,
CalloutModule,
],
})
export class UserVerificationDialogComponent {

View File

@@ -11,9 +11,9 @@ export type UserVerificationCalloutOptions = {
/**
* The type of the callout.
* Can be "warning", "danger", "error", or "tip".
* Can be "warning", "danger", "info", or "success".
*/
type: "warning" | "danger" | "error" | "tip";
type: "warning" | "danger" | "info" | "success";
};
/**

View File

@@ -49,12 +49,12 @@
</div>
</div>
<app-callout type="error" *ngIf="biometricsVerificationFailed">
<bit-callout type="danger" *ngIf="biometricsVerificationFailed">
{{ "couldNotCompleteBiometrics" | i18n }}
<button bitLink type="button" linkType="primary" (click)="verifyUserViaBiometrics()">
{{ "tryAgain" | i18n }}
</button>
</app-callout>
</bit-callout>
</ng-container>
<!-- Alternate verification options if user has more than 1 -->

View File

@@ -20,6 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
ButtonModule,
CalloutModule,
FormFieldModule,
IconButtonModule,
IconModule,
@@ -62,6 +63,7 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt
IconModule,
LinkModule,
ButtonModule,
CalloutModule,
],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil

View File

@@ -1,7 +1,12 @@
import { Observable } from "rxjs";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
export abstract class AuthRequestServiceAbstraction {
/** Emits an auth request id when an auth request has been approved. */
authRequestPushNotification$: Observable<string>;
/**
* Approve or deny an auth request.
* @param approve True to approve, false to deny.
@@ -54,4 +59,11 @@ export abstract class AuthRequestServiceAbstraction {
pubKeyEncryptedMasterKeyHash: string,
privateKey: ArrayBuffer,
) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>;
/**
* Handles incoming auth request push notifications.
* @param notification push notification.
* @remark We should only be receiving approved push notifications to prevent enumeration.
*/
abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void;
}

View File

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

View File

@@ -0,0 +1,38 @@
import { Observable } from "rxjs";
export abstract class LoginEmailServiceAbstraction {
/**
* An observable that monitors the storedEmail
*/
storedEmail$: Observable<string>;
/**
* Gets the current email being used in the login process.
* @returns A string of the email.
*/
getEmail: () => string;
/**
* Sets the current email being used in the login process.
* @param email The email to be set.
*/
setEmail: (email: string) => void;
/**
* Gets whether or not the email should be stored on disk.
* @returns A boolean stating whether or not the email should be stored on disk.
*/
getRememberEmail: () => boolean;
/**
* Sets whether or not the email should be stored on disk.
*/
setRememberEmail: (value: boolean) => void;
/**
* Sets the email and rememberEmail properties to null.
*/
clearValues: () => void;
/**
* - If rememberEmail is true, sets the storedEmail on disk to the current email.
* - If rememberEmail is false, sets the storedEmail on disk to null.
* - Then sets the email and rememberEmail properties to null.
* @returns A promise that resolves once the email settings are saved.
*/
saveEmailSettings: () => Promise<void>;
}

View File

@@ -4,7 +4,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { MasterKey } from "@bitwarden/common/types/key";
import {
@@ -21,10 +20,6 @@ export abstract class LoginStrategyServiceAbstraction {
* Emits null if the session has timed out.
*/
currentAuthType$: Observable<AuthenticationType | null>;
/**
* Emits when an auth request has been approved.
*/
authRequestPushNotification$: Observable<string>;
/**
* If the login strategy uses the email address of the user, this
* will return it. Otherwise, it will return null.
@@ -77,10 +72,6 @@ export abstract class LoginStrategyServiceAbstraction {
* Creates a master key from the provided master password and email.
*/
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
/**
* Sends a notification to {@link LoginStrategyServiceAbstraction.authRequestPushNotification}
*/
sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => Promise<void>;
/**
* Sends a response to an auth request.
*/

View File

@@ -0,0 +1,34 @@
import { Observable } from "rxjs";
import { UserDecryptionOptions } from "../models";
export abstract class UserDecryptionOptionsServiceAbstraction {
/**
* Returns what decryption options are available for the current user.
* @remark This is sent from the server on authentication.
*/
abstract userDecryptionOptions$: Observable<UserDecryptionOptions>;
/**
* Uses user decryption options to determine if current user has a master password.
* @remark This is sent from the server, and does not indicate if the master password
* was used to login and/or if a master key is saved locally.
*/
abstract hasMasterPassword$: Observable<boolean>;
/**
* Returns the user decryption options for the given user id.
* @param userId The user id to check.
*/
abstract userDecryptionOptionsById$(userId: string): Observable<UserDecryptionOptions>;
}
export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction {
/**
* Sets the current decryption options for the user, contains the current configuration
* of the users account related to how they can decrypt their vault.
* @remark Intended to be used when user decryption options are received from server, does
* not update the server. Consider syncing instead of updating locally.
* @param userDecryptionOptions Current user decryption options received from server.
*/
abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void>;
}

View File

@@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import {
@@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptions: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -65,6 +67,7 @@ describe("AuthRequestLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptions = mock<InternalUserDecryptionOptionsServiceAbstraction>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -83,6 +86,7 @@ describe("AuthRequestLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptions,
deviceTrustCryptoService,
billingAccountProfileStateService,
);

View File

@@ -16,7 +16,9 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -54,6 +56,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
@@ -67,6 +70,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);
@@ -125,8 +129,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
} else {
await this.trySetUserKeyWithMasterKey();
const userId = (await this.stateService.getUserId()) as UserId;
// Establish trust if required after setting user key
await this.deviceTrustCryptoService.trustDeviceIfRequired();
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
}
}

View File

@@ -28,7 +28,6 @@ import {
AccountProfile,
AccountTokens,
AccountKeys,
AccountDecryptionOptions,
} from "@bitwarden/common/platform/models/domain/account";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
@@ -37,10 +36,12 @@ import {
PasswordStrengthService,
} from "@bitwarden/common/tools/password-strength";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models";
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
@@ -108,6 +109,7 @@ describe("LoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -126,7 +128,7 @@ describe("LoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -146,6 +148,7 @@ describe("LoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,
@@ -183,9 +186,9 @@ describe("LoginStrategy", () => {
expect(tokenService.setTokens).toHaveBeenCalledWith(
accessToken,
refreshToken,
mockVaultTimeoutAction,
mockVaultTimeout,
refreshToken,
);
expect(stateService.addAccount).toHaveBeenCalledWith(
@@ -204,33 +207,12 @@ describe("LoginStrategy", () => {
...new AccountTokens(),
},
keys: new AccountKeys(),
decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse),
}),
);
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 }),
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
UserDecryptionOptions.fromResponse(idTokenResponse),
);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
it("builds AuthResult", async () => {
@@ -409,6 +391,7 @@ describe("LoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,

View File

@@ -26,13 +26,12 @@ 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 {
AccountKeys,
Account,
AccountProfile,
AccountTokens,
AccountDecryptionOptions,
} from "@bitwarden/common/platform/models/domain/account";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import {
UserApiLoginCredentials,
PasswordLoginCredentials,
@@ -40,6 +39,7 @@ import {
AuthRequestLoginCredentials,
WebAuthnLoginCredentials,
} from "../models/domain/login-credentials";
import { UserDecryptionOptions } from "../models/domain/user-decryption-options";
import { CacheData } from "../services/login-strategies/login-strategy.state";
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
@@ -69,6 +69,7 @@ export abstract class LoginStrategy {
protected logService: LogService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
@@ -158,18 +159,8 @@ export abstract class LoginStrategy {
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<void> {
const accountInformation = await this.tokenService.decodeAccessToken(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 });
@@ -180,9 +171,9 @@ export abstract class LoginStrategy {
// User id will be derived from the access token.
await this.tokenService.setTokens(
tokenResponse.accessToken,
tokenResponse.refreshToken,
vaultTimeoutAction as VaultTimeoutAction,
vaultTimeout,
tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token.
);
await this.stateService.addAccount(
@@ -202,12 +193,14 @@ export abstract class LoginStrategy {
tokens: {
...new AccountTokens(),
},
keys: accountKeys,
decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse),
adminAuthRequest: adminAuthRequest?.toJSON(),
}),
);
await this.userDecryptionOptionsService.setUserDecryptionOptions(
UserDecryptionOptions.fromResponse(tokenResponse),
);
await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false);
}

View File

@@ -27,6 +27,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -60,6 +61,7 @@ describe("PasswordLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let policyService: MockProxy<PolicyService>;
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
policyService = mock<PolicyService>();
passwordStrengthService = mock<PasswordStrengthService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,

View File

@@ -26,6 +26,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { MasterKey } from "@bitwarden/common/types/key";
import { LoginStrategyServiceAbstraction } from "../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -36,15 +37,11 @@ export class PasswordLoginStrategyData implements LoginStrategyData {
/** User's entered email obtained pre-login. Always present in MP login. */
userEnteredEmail: string;
/** If 2fa is required, token is returned to bypass captcha */
captchaBypassToken?: string;
/**
* The local version of the user's master key hash
*/
/** The local version of the user's master key hash */
localMasterKeyHash: string;
/**
* The user's master key
*/
/** The user's master key */
masterKey: MasterKey;
/**
* Tracks if the user needs to update their password due to
@@ -62,14 +59,12 @@ export class PasswordLoginStrategyData implements LoginStrategyData {
}
export class PasswordLoginStrategy extends LoginStrategy {
/**
* The email address of the user attempting to log in.
*/
/** The email address of the user attempting to log in. */
email$: Observable<string>;
/**
* The master key hash of the user attempting to log in.
*/
masterKeyHash$: Observable<string | null>;
/** The master key hash used for authentication */
serverMasterKeyHash$: Observable<string>;
/** The local master key hash we store client side */
localMasterKeyHash$: Observable<string | null>;
protected cache: BehaviorSubject<PasswordLoginStrategyData>;
@@ -84,6 +79,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService: LogService,
protected stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService,
private loginStrategyService: LoginStrategyServiceAbstraction,
@@ -99,12 +95,16 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);
this.cache = new BehaviorSubject(data);
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
this.masterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
this.serverMasterKeyHash$ = this.cache.pipe(
map((state) => state.tokenRequest.masterPasswordHash),
);
this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
}
override async logIn(credentials: PasswordLoginCredentials) {
@@ -120,11 +120,14 @@ export class PasswordLoginStrategy extends LoginStrategy {
data.masterKey,
HashPurpose.LocalAuthorization,
);
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, data.masterKey);
const serverMasterKeyHash = await this.cryptoService.hashMasterKey(
masterPassword,
data.masterKey,
);
data.tokenRequest = new PasswordTokenRequest(
email,
masterKeyHash,
serverMasterKeyHash,
captchaToken,
await this.buildTwoFactor(twoFactor, email),
await this.buildDeviceRequest(),
@@ -168,10 +171,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
twoFactor: TokenTwoFactorRequest,
captchaResponse: string,
): Promise<AuthResult> {
this.cache.next({
...this.cache.value,
captchaBypassToken: captchaResponse ?? this.cache.value.captchaBypassToken,
});
const data = this.cache.value;
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
this.cache.next(data);
const result = await super.logInTwoFactor(twoFactor);
// 2FA was successful, save the force update password options with the state service if defined

View File

@@ -23,7 +23,10 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction } from "../abstractions";
import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -39,6 +42,7 @@ describe("SsoLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
@@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>();
@@ -87,6 +92,7 @@ describe("SsoLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
keyConnectorService,
deviceTrustCryptoService,
authRequestService,

View File

@@ -20,8 +20,12 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { AuthRequestServiceAbstraction } from "../abstractions";
import {
InternalUserDecryptionOptionsServiceAbstraction,
AuthRequestServiceAbstraction,
} from "../abstractions";
import { SsoLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -84,6 +88,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private keyConnectorService: KeyConnectorService,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction,
@@ -100,6 +105,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);
@@ -279,7 +285,8 @@ export class SsoLoginStrategy extends LoginStrategy {
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();
const userId = (await this.stateService.getUserId()) as UserId;
await this.deviceTrustCryptoService.trustDeviceIfRequired(userId);
// 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
@@ -293,7 +300,9 @@ export class SsoLoginStrategy extends LoginStrategy {
private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise<void> {
const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption;
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey();
const userId = (await this.stateService.getUserId()) as UserId;
const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(userId);
const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey;
const encUserKey = trustedDeviceOption?.encryptedUserKey;
@@ -302,6 +311,7 @@ export class SsoLoginStrategy extends LoginStrategy {
}
const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey(
userId,
encDevicePrivateKey,
encUserKey,
deviceKey,

View File

@@ -1,4 +1,5 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
@@ -8,7 +9,10 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
Environment,
EnvironmentService,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -18,6 +22,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -35,6 +40,7 @@ describe("UserApiLoginStrategy", () => {
let logService: MockProxy<LogService>;
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let keyConnectorService: MockProxy<KeyConnectorService>;
let environmentService: MockProxy<EnvironmentService>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
@@ -57,6 +63,7 @@ describe("UserApiLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
keyConnectorService = mock<KeyConnectorService>();
environmentService = mock<EnvironmentService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
@@ -76,6 +83,7 @@ describe("UserApiLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
environmentService,
keyConnectorService,
billingAccountProfileStateService,
@@ -141,8 +149,11 @@ describe("UserApiLoginStrategy", () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
const env = mock<Environment>();
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
environmentService.environment$ = new BehaviorSubject(env);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
await apiLogInStrategy.logIn(credentials);
@@ -156,8 +167,11 @@ describe("UserApiLoginStrategy", () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
const env = mock<Environment>();
env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
environmentService.environment$ = new BehaviorSubject(env);
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
cryptoService.getMasterKey.mockResolvedValue(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);

View File

@@ -1,4 +1,4 @@
import { BehaviorSubject } from "rxjs";
import { firstValueFrom, BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -47,6 +48,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService,
billingAccountProfileStateService: BillingAccountProfileStateService,
@@ -61,6 +63,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);
this.cache = new BehaviorSubject(data);
@@ -82,7 +85,8 @@ export class UserApiLoginStrategy extends LoginStrategy {
protected override async setMasterKey(response: IdentityTokenResponse) {
if (response.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
const env = await firstValueFrom(this.environmentService.environment$);
const keyConnectorUrl = env.getKeyConnectorUrl();
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
}
}

View File

@@ -18,6 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PrfKey, UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { identityTokenResponseFactory } from "./login.strategy.spec";
@@ -35,6 +36,7 @@ describe("WebAuthnLoginStrategy", () => {
let logService!: MockProxy<LogService>;
let stateService!: MockProxy<StateService>;
let twoFactorService!: MockProxy<TwoFactorService>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
@@ -70,6 +72,7 @@ describe("WebAuthnLoginStrategy", () => {
logService = mock<LogService>();
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
tokenService.getTwoFactorToken.mockResolvedValue(null);
@@ -87,6 +90,7 @@ describe("WebAuthnLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -17,6 +17,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserKey } from "@bitwarden/common/types/key";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions";
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -49,6 +50,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
super(
@@ -61,6 +63,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -1,2 +1,3 @@
export * from "./rotateable-key-set";
export * from "./login-credentials";
export * from "./user-decryption-options";

View File

@@ -0,0 +1,165 @@
import { Jsonify } from "type-fest";
import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response";
import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response";
import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response";
/**
* Key Connector decryption options. Intended to be sent to the client for use after authentication.
* @see {@link UserDecryptionOptions}
*/
export class KeyConnectorUserDecryptionOption {
/** The URL of the key connector configured for this user. */
keyConnectorUrl: string;
/**
* Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object.
* @param response The key connector user decryption option response object.
* @returns A new instance of the KeyConnectorUserDecryptionOption or undefined if `response` is nullish.
*/
static fromResponse(
response: KeyConnectorUserDecryptionOptionResponse,
): KeyConnectorUserDecryptionOption | undefined {
if (response == null) {
return undefined;
}
const options = new KeyConnectorUserDecryptionOption();
options.keyConnectorUrl = response?.keyConnectorUrl ?? null;
return options;
}
/**
* Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the KeyConnectorUserDecryptionOption or undefined if `obj` is nullish.
*/
static fromJSON(
obj: Jsonify<KeyConnectorUserDecryptionOption>,
): KeyConnectorUserDecryptionOption | undefined {
if (obj == null) {
return undefined;
}
return Object.assign(new KeyConnectorUserDecryptionOption(), obj);
}
}
/**
* Trusted device decryption options. Intended to be sent to the client for use after authentication.
* @see {@link UserDecryptionOptions}
*/
export class TrustedDeviceUserDecryptionOption {
/** True if an admin has approved an admin auth request previously made from this device. */
hasAdminApproval: boolean;
/** True if the user has a device capable of approving an auth request. */
hasLoginApprovingDevice: boolean;
/** True if the user has manage reset password permission, as these users must be forced to have a master password. */
hasManageResetPasswordPermission: boolean;
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object.
* @param response The trusted device user decryption option response object.
* @returns A new instance of the TrustedDeviceUserDecryptionOption or undefined if `response` is nullish.
*/
static fromResponse(
response: TrustedDeviceUserDecryptionOptionResponse,
): TrustedDeviceUserDecryptionOption | undefined {
if (response == null) {
return undefined;
}
const options = new TrustedDeviceUserDecryptionOption();
options.hasAdminApproval = response?.hasAdminApproval ?? false;
options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false;
options.hasManageResetPasswordPermission = response?.hasManageResetPasswordPermission ?? false;
return options;
}
/**
* Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the TrustedDeviceUserDecryptionOption or undefined if `obj` is nullish.
*/
static fromJSON(
obj: Jsonify<TrustedDeviceUserDecryptionOption>,
): TrustedDeviceUserDecryptionOption | undefined {
if (obj == null) {
return undefined;
}
return Object.assign(new TrustedDeviceUserDecryptionOption(), obj);
}
}
/**
* Represents the decryption options the user has configured on the server. This is intended to be sent
* to the client on authentication, and can be used to determine how to decrypt the user's vault.
*/
export class UserDecryptionOptions {
/** True if the user has a master password configured on the server. */
hasMasterPassword: boolean;
/** {@link TrustedDeviceUserDecryptionOption} */
trustedDeviceOption?: TrustedDeviceUserDecryptionOption;
/** {@link KeyConnectorUserDecryptionOption} */
keyConnectorOption?: KeyConnectorUserDecryptionOption;
/**
* Initializes a new instance of the UserDecryptionOptions from a response object.
* @param response user decryption options response object
* @returns A new instance of the UserDecryptionOptions.
* @throws If the response is nullish, this method will throw an error. User decryption options
* are required for client initialization.
*/
// TODO: Change response type to `UserDecryptionOptionsResponse` after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
static fromResponse(response: IdentityTokenResponse): UserDecryptionOptions {
if (response == null) {
throw new Error("User Decryption Options are required for client initialization.");
}
const decryptionOptions = new UserDecryptionOptions();
if (response.userDecryptionOptions) {
// If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate
// the new decryption options.
const responseOptions = response.userDecryptionOptions;
decryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword;
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromResponse(
responseOptions.trustedDeviceOption,
);
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromResponse(
responseOptions.keyConnectorOption,
);
} else {
// If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so
// we must base our decryption options on the presence of the keyConnectorUrl.
// Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE
// server versions, a master password short-circuited the addition of the keyConnectorUrl to the response.
// TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
const usingKeyConnector = response.keyConnectorUrl != null;
decryptionOptions.hasMasterPassword = !usingKeyConnector;
if (usingKeyConnector) {
decryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption();
decryptionOptions.keyConnectorOption.keyConnectorUrl = response.keyConnectorUrl;
}
}
return decryptionOptions;
}
/**
* Initializes a new instance of the UserDecryptionOptions from a JSON object.
* @param obj JSON object to deserialize.
* @returns A new instance of the UserDecryptionOptions. Will initialize even if the JSON object is nullish.
*/
static fromJSON(obj: Jsonify<UserDecryptionOptions>): UserDecryptionOptions {
const decryptionOptions = Object.assign(new UserDecryptionOptions(), obj);
decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromJSON(
obj?.trustedDeviceOption,
);
decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromJSON(
obj?.keyConnectorOption,
);
return decryptionOptions;
}
}

View File

@@ -1 +1,2 @@
export * from "./domain";
export * from "./spec";

View File

@@ -0,0 +1,38 @@
import {
KeyConnectorUserDecryptionOption,
TrustedDeviceUserDecryptionOption,
UserDecryptionOptions,
} from "../domain";
// To discourage creating new user decryption options, we don't expose a constructor.
// These helpers are for testing purposes only.
/** Testing helper for creating new instances of `UserDecryptionOptions` */
export class FakeUserDecryptionOptions extends UserDecryptionOptions {
constructor(init: Partial<UserDecryptionOptions>) {
super();
Object.assign(this, init);
}
}
/** Testing helper for creating new instances of `KeyConnectorUserDecryptionOption` */
export class FakeKeyConnectorUserDecryptionOption extends KeyConnectorUserDecryptionOption {
constructor(keyConnectorUrl: string) {
super();
this.keyConnectorUrl = keyConnectorUrl;
}
}
/** Testing helper for creating new instances of `TrustedDeviceUserDecryptionOption` */
export class FakeTrustedDeviceUserDecryptionOption extends TrustedDeviceUserDecryptionOption {
constructor(
hasAdminApproval: boolean,
hasLoginApprovingDevice: boolean,
hasManageResetPasswordPermission: boolean,
) {
super();
this.hasAdminApproval = hasAdminApproval;
this.hasLoginApprovingDevice = hasLoginApprovingDevice;
this.hasManageResetPasswordPermission = hasManageResetPasswordPermission;
}
}

View File

@@ -0,0 +1 @@
export * from "./fake-user-decryption-options";

View File

@@ -2,6 +2,7 @@ import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -30,6 +31,22 @@ describe("AuthRequestService", () => {
mockPrivateKey = new Uint8Array(64);
});
describe("authRequestPushNotification$", () => {
it("should emit when sendAuthRequestPushNotification is called", () => {
const notification = {
id: "PUSH_NOTIFICATION",
userId: "USER_ID",
} as AuthRequestPushNotification;
const spy = jest.fn();
sut.authRequestPushNotification$.subscribe(spy);
sut.sendAuthRequestPushNotification(notification);
expect(spy).toHaveBeenCalledWith("PUSH_NOTIFICATION");
});
});
describe("approveOrDenyAuthRequest", () => {
beforeEach(() => {
cryptoService.rsaEncrypt.mockResolvedValue({

View File

@@ -1,6 +1,9 @@
import { Observable, Subject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -11,12 +14,17 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
export class AuthRequestService implements AuthRequestServiceAbstraction {
private authRequestPushNotificationSubject = new Subject<string>();
authRequestPushNotification$: Observable<string>;
constructor(
private appIdService: AppIdService,
private cryptoService: CryptoService,
private apiService: ApiService,
private stateService: StateService,
) {}
) {
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
}
async approveOrDenyAuthRequest(
approve: boolean,
@@ -126,4 +134,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
masterKeyHash,
};
}
sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void {
if (notification.id != null) {
this.authRequestPushNotificationSubject.next(notification.id);
}
}
}

View File

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

View File

@@ -0,0 +1,52 @@
import { Observable } from "rxjs";
import {
GlobalState,
KeyDefinition,
LOGIN_EMAIL_DISK,
StateProvider,
} from "../../../../../common/src/platform/state";
import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service";
const STORED_EMAIL = new KeyDefinition<string>(LOGIN_EMAIL_DISK, "storedEmail", {
deserializer: (value: string) => value,
});
export class LoginEmailService implements LoginEmailServiceAbstraction {
private email: string;
private rememberEmail: boolean;
private readonly storedEmailState: GlobalState<string>;
storedEmail$: Observable<string>;
constructor(private stateProvider: StateProvider) {
this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL);
this.storedEmail$ = this.storedEmailState.state$;
}
getEmail() {
return this.email;
}
setEmail(email: string) {
this.email = email;
}
getRememberEmail() {
return this.rememberEmail;
}
setRememberEmail(value: boolean) {
this.rememberEmail = value;
}
clearValues() {
this.email = null;
this.rememberEmail = null;
}
async saveEmailSettings() {
await this.storedEmailState.update(() => (this.rememberEmail ? this.email : null));
this.clearValues();
}
}

View File

@@ -25,8 +25,12 @@ import { KdfType } from "@bitwarden/common/platform/enums";
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { AuthRequestServiceAbstraction } from "../../abstractions";
import {
AuthRequestServiceAbstraction,
InternalUserDecryptionOptionsServiceAbstraction,
} from "../../abstractions";
import { PasswordLoginCredentials } from "../../models";
import { UserDecryptionOptionsService } from "../user-decryption-options/user-decryption-options.service";
import { LoginStrategyService } from "./login-strategy.service";
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
@@ -51,6 +55,7 @@ describe("LoginStrategyService", () => {
let policyService: MockProxy<PolicyService>;
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
let stateProvider: FakeGlobalStateProvider;
@@ -74,6 +79,7 @@ describe("LoginStrategyService", () => {
policyService = mock<PolicyService>();
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
authRequestService = mock<AuthRequestServiceAbstraction>();
userDecryptionOptionsService = mock<UserDecryptionOptionsService>();
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
stateProvider = new FakeGlobalStateProvider();
@@ -95,6 +101,7 @@ describe("LoginStrategyService", () => {
policyService,
deviceTrustCryptoService,
authRequestService,
userDecryptionOptionsService,
stateProvider,
billingAccountProfileStateService,
);

View File

@@ -1,7 +1,6 @@
import {
combineLatestWith,
distinctUntilChanged,
filter,
firstValueFrom,
map,
Observable,
@@ -23,7 +22,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@@ -40,6 +38,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass
import { MasterKey } from "@bitwarden/common/types/key";
import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy";
import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy";
import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy";
@@ -80,8 +79,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
>;
currentAuthType$: Observable<AuthenticationType | null>;
// TODO: move to auth request service
authRequestPushNotification$: Observable<string>;
constructor(
protected cryptoService: CryptoService,
@@ -101,6 +98,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected stateProvider: GlobalStateProvider,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {
@@ -112,9 +110,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
);
this.currentAuthType$ = this.currentAuthnTypeState.state$;
this.authRequestPushNotification$ = this.authRequestPushNotificationState.state$.pipe(
filter((id) => id != null),
);
this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe(
distinctUntilChanged(),
combineLatestWith(this.loginStrategyCacheState.state$),
@@ -135,8 +130,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
async getMasterPasswordHash(): Promise<string | null> {
const strategy = await firstValueFrom(this.loginStrategy$);
if ("masterKeyHash$" in strategy) {
return await firstValueFrom(strategy.masterKeyHash$);
if ("serverMasterKeyHash$" in strategy) {
return await firstValueFrom(strategy.serverMasterKeyHash$);
}
return null;
}
@@ -254,13 +249,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
}
// TODO move to auth request service
async sendAuthRequestPushNotification(notification: AuthRequestPushNotification): Promise<void> {
if (notification.id != null) {
await this.authRequestPushNotificationState.update((_) => notification.id);
}
}
// TODO: move to auth request service
async passwordlessLogin(
id: string,
@@ -354,6 +342,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.passwordStrengthService,
this.policyService,
this,
@@ -371,6 +360,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authRequestService,
@@ -389,6 +379,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.environmentService,
this.keyConnectorService,
this.billingAccountProfileStateService,
@@ -405,6 +396,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.deviceTrustCryptoService,
this.billingAccountProfileStateService,
);
@@ -420,6 +412,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.billingAccountProfileStateService,
);
}

View File

@@ -0,0 +1,94 @@
import { firstValueFrom } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import {
USER_DECRYPTION_OPTIONS,
UserDecryptionOptionsService,
} from "./user-decryption-options.service";
describe("UserDecryptionOptionsService", () => {
let sut: UserDecryptionOptionsService;
const fakeUserId = Utils.newGuid() as UserId;
let fakeAccountService: FakeAccountService;
let fakeStateProvider: FakeStateProvider;
beforeEach(() => {
fakeAccountService = mockAccountServiceWith(fakeUserId);
fakeStateProvider = new FakeStateProvider(fakeAccountService);
sut = new UserDecryptionOptionsService(fakeStateProvider);
});
const userDecryptionOptions = {
hasMasterPassword: true,
trustedDeviceOption: {
hasAdminApproval: false,
hasLoginApprovingDevice: false,
hasManageResetPasswordPermission: true,
},
keyConnectorOption: {
keyConnectorUrl: "https://keyconnector.bitwarden.com",
},
};
describe("userDecryptionOptions$", () => {
it("should return the active user's decryption options", async () => {
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
const result = await firstValueFrom(sut.userDecryptionOptions$);
expect(result).toEqual(userDecryptionOptions);
});
});
describe("hasMasterPassword$", () => {
it("should return the hasMasterPassword property of the active user's decryption options", async () => {
await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions);
const result = await firstValueFrom(sut.hasMasterPassword$);
expect(result).toBe(true);
});
});
describe("userDecryptionOptionsById$", () => {
it("should return the user decryption options for the given user", async () => {
const givenUser = Utils.newGuid() as UserId;
await fakeAccountService.addAccount(givenUser, {
name: "Test User 1",
email: "test1@email.com",
status: AuthenticationStatus.Locked,
});
await fakeStateProvider.setUserState(
USER_DECRYPTION_OPTIONS,
userDecryptionOptions,
givenUser,
);
const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser));
expect(result).toEqual(userDecryptionOptions);
});
});
describe("setUserDecryptionOptions", () => {
it("should set the active user's decryption options", async () => {
await sut.setUserDecryptionOptions(userDecryptionOptions);
const result = await firstValueFrom(
fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$,
);
expect(result).toEqual(userDecryptionOptions);
});
});
});

View File

@@ -0,0 +1,47 @@
import { map } from "rxjs";
import {
ActiveUserState,
StateProvider,
USER_DECRYPTION_OPTIONS_DISK,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/src/types/guid";
import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction";
import { UserDecryptionOptions } from "../../models";
export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptions>(
USER_DECRYPTION_OPTIONS_DISK,
"decryptionOptions",
{
deserializer: (decryptionOptions) => UserDecryptionOptions.fromJSON(decryptionOptions),
clearOn: ["logout"],
},
);
export class UserDecryptionOptionsService
implements InternalUserDecryptionOptionsServiceAbstraction
{
private userDecryptionOptionsState: ActiveUserState<UserDecryptionOptions>;
userDecryptionOptions$;
hasMasterPassword$;
constructor(private stateProvider: StateProvider) {
this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS);
this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$;
this.hasMasterPassword$ = this.userDecryptionOptions$.pipe(
map((options) => options?.hasMasterPassword ?? false),
);
}
userDecryptionOptionsById$(userId: UserId) {
return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$;
}
async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise<void> {
await this.userDecryptionOptionsState.update((_) => userDecryptionOptions);
}
}