1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

[PM-5404, PM-3518] Migrate user decryption options to new service (#7344)

* create new user decryption options service

* rename new service to user decryption options

* add hasMasterPassword to user decryption options service

* migrate device trust service to new user decryption options service

* add migration for user-decryption-options

* migrate sync service and calls to trust-device-service

* rename abstraction file

* migrate two factor component

* migrate two factor spec

* migrate sso component

* migrate set-password component

* migrate base login decryption component

* migrate organization options component

* fix component imports

* add missing imports
- remove state service calls
- add update user decryption options method

* remove acct decryption options from account

* lint

* fix tests and linting

* fix browser

* fix desktop

* add user decryption options service to cli

* remove default value from migration

* bump migration number

* fix merge conflict

* fix vault timeout settings

* fix cli

* more fixes

* add user decryption options service to deps of vault timeout settings service

* update login strategy service with user decryption options

* remove early return from sync bandaid for user decryption options

* move user decryption options service to lib/auth

* move user decryption options to libs/auth

* fix reference

* fix browser

* check user decryption options after 2fa check

* update migration and revert tsconfig changes

* add more documentation

* clear user decryption options on logout

* fix tests by creating helper for user decryption options

* fix tests

* pr feedback

* fix factory

* update migration

* add tests

* update missed migration num in test
This commit is contained in:
Jake Fink
2024-03-20 20:33:57 -04:00
committed by GitHub
parent e2fe1e1567
commit 2111b37c32
68 changed files with 1158 additions and 360 deletions

View File

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

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

@@ -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 { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
import { CacheData } from "../services/login-strategies/login-strategy.state";
@@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
billingAccountProfileStateService: BillingAccountProfileStateService,
) {
@@ -67,6 +69,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

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";
@@ -39,8 +38,10 @@ import {
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserKey, MasterKey, DeviceKey } 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,
@@ -204,9 +207,11 @@ describe("LoginStrategy", () => {
...new AccountTokens(),
},
keys: new AccountKeys(),
decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse),
}),
);
expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith(
UserDecryptionOptions.fromResponse(idTokenResponse),
);
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
});
@@ -409,6 +414,7 @@ describe("LoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
passwordStrengthService,
policyService,
loginStrategyService,

View File

@@ -30,9 +30,9 @@ import {
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 +40,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 +70,7 @@ export abstract class LoginStrategy {
protected logService: LogService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
@@ -203,11 +205,14 @@ export abstract class LoginStrategy {
...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";
@@ -84,6 +85,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,6 +101,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

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

@@ -21,7 +21,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 { 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 +87,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 +104,7 @@ export class SsoLoginStrategy extends LoginStrategy {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
billingAccountProfileStateService,
);

View File

@@ -18,6 +18,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 +36,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 +59,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 +79,7 @@ describe("UserApiLoginStrategy", () => {
logService,
stateService,
twoFactorService,
userDecryptionOptionsService,
environmentService,
keyConnectorService,
billingAccountProfileStateService,

View File

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

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,153 @@
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. Will initialize even if the response is nullish.
*/
static fromResponse(
response: KeyConnectorUserDecryptionOptionResponse,
): KeyConnectorUserDecryptionOption {
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. Will initialize even if the JSON object is nullish.
*/
static fromJSON(
obj: Jsonify<KeyConnectorUserDecryptionOption>,
): KeyConnectorUserDecryptionOption {
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. Will initialize even if the response is nullish.
*/
static fromResponse(
response: TrustedDeviceUserDecryptionOptionResponse,
): TrustedDeviceUserDecryptionOption {
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. Will initialize even if the JSON object is nullish.
*/
static fromJSON(
obj: Jsonify<TrustedDeviceUserDecryptionOption>,
): TrustedDeviceUserDecryptionOption {
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

@@ -1,3 +1,4 @@
export * from "./pin-crypto/pin-crypto.service.implementation";
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

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

@@ -40,6 +40,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";
@@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
protected policyService: PolicyService,
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
protected authRequestService: AuthRequestServiceAbstraction,
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
protected stateProvider: GlobalStateProvider,
protected billingAccountProfileStateService: BillingAccountProfileStateService,
) {
@@ -354,6 +356,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.passwordStrengthService,
this.policyService,
this,
@@ -371,6 +374,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.keyConnectorService,
this.deviceTrustCryptoService,
this.authRequestService,
@@ -389,6 +393,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.environmentService,
this.keyConnectorService,
this.billingAccountProfileStateService,
@@ -405,6 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.userDecryptionOptionsService,
this.deviceTrustCryptoService,
this.billingAccountProfileStateService,
);
@@ -420,6 +426,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);
}
}