mirror of
https://github.com/bitwarden/browser
synced 2026-02-21 11:54:02 +00:00
Merge branch 'main' into km/replace-encstring-with-unsigned-shared-key
This commit is contained in:
@@ -6,19 +6,26 @@ import { ReplaySubject, combineLatest, map, Observable } from "rxjs";
|
||||
import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
|
||||
import { UserId } from "../src/types/guid";
|
||||
|
||||
/**
|
||||
* Creates a mock AccountInfo object with sensible defaults that can be overridden.
|
||||
* Use this when you need just an AccountInfo object in tests.
|
||||
*/
|
||||
export function mockAccountInfoWith(info: Partial<AccountInfo> = {}): AccountInfo {
|
||||
return {
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
export function mockAccountServiceWith(
|
||||
userId: UserId,
|
||||
info: Partial<AccountInfo> = {},
|
||||
activity: Record<UserId, Date> = {},
|
||||
): FakeAccountService {
|
||||
const fullInfo: AccountInfo = {
|
||||
...info,
|
||||
...{
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: true,
|
||||
},
|
||||
};
|
||||
const fullInfo = mockAccountInfoWith(info);
|
||||
|
||||
const fullActivity = { [userId]: new Date(), ...activity };
|
||||
|
||||
@@ -104,6 +111,10 @@ export class FakeAccountService implements AccountService {
|
||||
await this.mock.setAccountEmailVerified(userId, emailVerified);
|
||||
}
|
||||
|
||||
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
|
||||
await this.mock.setAccountCreationDate(userId, creationDate);
|
||||
}
|
||||
|
||||
async switchAccount(userId: UserId): Promise<void> {
|
||||
const next =
|
||||
userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] };
|
||||
@@ -127,4 +138,5 @@ const loggedOutInfo: AccountInfo = {
|
||||
name: undefined,
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
creationDate: undefined,
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ import { UpdateProfileRequest } from "../auth/models/request/update-profile.requ
|
||||
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentitySsoRequiredResponse } from "../auth/models/response/identity-sso-required.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
@@ -140,7 +141,10 @@ export abstract class ApiService {
|
||||
| UserApiTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
): Promise<
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
| IdentitySsoRequiredResponse
|
||||
>;
|
||||
abstract refreshIdentityToken(userId?: UserId): Promise<any>;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export class PolicyData {
|
||||
type: PolicyType;
|
||||
data: Record<string, string | number | boolean>;
|
||||
enabled: boolean;
|
||||
revisionDate: string;
|
||||
|
||||
constructor(response?: PolicyResponse) {
|
||||
if (response == null) {
|
||||
@@ -22,6 +23,7 @@ export class PolicyData {
|
||||
this.type = response.type;
|
||||
this.data = response.data;
|
||||
this.enabled = response.enabled;
|
||||
this.revisionDate = response.revisionDate;
|
||||
}
|
||||
|
||||
static fromPolicy(policy: Policy): PolicyData {
|
||||
|
||||
@@ -19,6 +19,8 @@ export class Policy extends Domain {
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
revisionDate: Date;
|
||||
|
||||
constructor(obj?: PolicyData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
@@ -30,6 +32,7 @@ export class Policy extends Domain {
|
||||
this.type = obj.type;
|
||||
this.data = obj.data;
|
||||
this.enabled = obj.enabled;
|
||||
this.revisionDate = new Date(obj.revisionDate);
|
||||
}
|
||||
|
||||
static fromResponse(response: PolicyResponse): Policy {
|
||||
|
||||
@@ -9,6 +9,7 @@ export class PolicyResponse extends BaseResponse {
|
||||
data: any;
|
||||
enabled: boolean;
|
||||
canToggleState: boolean;
|
||||
revisionDate: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -18,5 +19,6 @@ export class PolicyResponse extends BaseResponse {
|
||||
this.data = this.getResponseProperty("Data");
|
||||
this.enabled = this.getResponseProperty("Enabled");
|
||||
this.canToggleState = this.getResponseProperty("CanToggleState") ?? true;
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,7 @@ function buildKeyDefinition<T>(key: string): UserKeyDefinition<T> {
|
||||
|
||||
export const AUTO_CONFIRM_FINGERPRINTS = buildKeyDefinition<boolean>("autoConfirmFingerPrints");
|
||||
|
||||
export class DefaultOrganizationManagementPreferencesService
|
||||
implements OrganizationManagementPreferencesService
|
||||
{
|
||||
export class DefaultOrganizationManagementPreferencesService implements OrganizationManagementPreferencesService {
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
autoConfirmFingerPrints = this.buildOrganizationManagementPreference(
|
||||
|
||||
@@ -83,12 +83,15 @@ describe("PolicyService", () => {
|
||||
type: PolicyType.MaximumVaultTimeout,
|
||||
enabled: true,
|
||||
data: { minutes: 14 },
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "99",
|
||||
organizationId: "test-organization",
|
||||
type: PolicyType.DisableSend,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -113,6 +116,8 @@ describe("PolicyService", () => {
|
||||
organizationId: "test-organization",
|
||||
type: PolicyType.DisableSend,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -242,6 +247,8 @@ describe("PolicyService", () => {
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,24 +338,32 @@ describe("PolicyService", () => {
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.ActivateAutofill,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -371,24 +386,32 @@ describe("PolicyService", () => {
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.ActivateAutofill,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: false,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -411,24 +434,32 @@ describe("PolicyService", () => {
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.ActivateAutofill,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org5",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org2",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -451,24 +482,32 @@ describe("PolicyService", () => {
|
||||
organizationId: "org4",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy2",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.ActivateAutofill,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy3",
|
||||
organizationId: "org3",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
{
|
||||
id: "policy4",
|
||||
organizationId: "org1",
|
||||
type: PolicyType.DisablePersonalVaultExport,
|
||||
enabled: true,
|
||||
data: undefined,
|
||||
revisionDate: expect.any(Date),
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -788,6 +827,7 @@ describe("PolicyService", () => {
|
||||
policyData.type = type;
|
||||
policyData.enabled = enabled;
|
||||
policyData.data = data;
|
||||
policyData.revisionDate = new Date().toISOString();
|
||||
|
||||
return policyData;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,11 @@ import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
/**
|
||||
* Holds information about an account for use in the AccountService
|
||||
* if more information is added, be sure to update the equality method.
|
||||
*/
|
||||
export type AccountInfo = {
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
name: string | undefined;
|
||||
creationDate: string | undefined;
|
||||
};
|
||||
|
||||
export type Account = { id: UserId } & AccountInfo;
|
||||
@@ -75,6 +72,12 @@ export abstract class AccountService {
|
||||
* @param emailVerified
|
||||
*/
|
||||
abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise<void>;
|
||||
/**
|
||||
* updates the `accounts$` observable with the creation date for the account.
|
||||
* @param userId
|
||||
* @param creationDate
|
||||
*/
|
||||
abstract setAccountCreationDate(userId: UserId, creationDate: string): Promise<void>;
|
||||
/**
|
||||
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
|
||||
* @param userId
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
||||
|
||||
@@ -18,10 +20,16 @@ export class AuthResult {
|
||||
email: string;
|
||||
requiresEncryptionKeyMigration: boolean;
|
||||
requiresDeviceVerification: boolean;
|
||||
ssoOrganizationIdentifier?: string | null;
|
||||
// The master-password used in the authentication process
|
||||
masterPassword: string | null;
|
||||
|
||||
get requiresTwoFactor() {
|
||||
return this.twoFactorProviders != null;
|
||||
}
|
||||
|
||||
// This is not as extensible as an object-based approach. In the future we may need to adjust to an object based approach.
|
||||
get requiresSso() {
|
||||
return !Utils.isNullOrWhitespace(this.ssoOrganizationIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class IdentitySsoRequiredResponse extends BaseResponse {
|
||||
ssoOrganizationIdentifier: string | null;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.ssoOrganizationIdentifier = this.getResponseProperty("SsoOrganizationIdentifier");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
|
||||
import { FakeGlobalState } from "../../../spec/fake-state";
|
||||
import {
|
||||
FakeGlobalStateProvider,
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
} from "./account.service";
|
||||
|
||||
describe("accountInfoEqual", () => {
|
||||
const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true };
|
||||
const accountInfo = mockAccountInfoWith();
|
||||
|
||||
it("compares nulls", () => {
|
||||
expect(accountInfoEqual(null, null)).toBe(true);
|
||||
@@ -64,6 +65,23 @@ describe("accountInfoEqual", () => {
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares creationDate", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, creationDate: "2024-12-31T00:00:00.000Z" };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares undefined creationDate", () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({ creationDate: undefined });
|
||||
const same = { ...accountWithoutCreationDate };
|
||||
const different = { ...accountWithoutCreationDate, creationDate: "2024-01-01T00:00:00.000Z" };
|
||||
|
||||
expect(accountInfoEqual(accountWithoutCreationDate, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountWithoutCreationDate, different)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("accountService", () => {
|
||||
@@ -76,7 +94,10 @@ describe("accountService", () => {
|
||||
let activeAccountIdState: FakeGlobalState<UserId>;
|
||||
let accountActivityState: FakeGlobalState<Record<UserId, Date>>;
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const userInfo = { email: "email", name: "name", emailVerified: true };
|
||||
const userInfo = mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
messagingService = mock();
|
||||
@@ -253,6 +274,56 @@ describe("accountService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("setCreationDate", () => {
|
||||
const initialState = { [userId]: userInfo };
|
||||
beforeEach(() => {
|
||||
accountsState.stateSubject.next(initialState);
|
||||
});
|
||||
|
||||
it("should update the account with a new creation date", async () => {
|
||||
const newCreationDate = "2024-12-31T00:00:00.000Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo, creationDate: newCreationDate },
|
||||
});
|
||||
});
|
||||
|
||||
it("should not update if the creation date is the same", async () => {
|
||||
await sut.setAccountCreationDate(userId, userInfo.creationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual(initialState);
|
||||
});
|
||||
|
||||
it("should update from undefined to a defined creation date", async () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({
|
||||
...userInfo,
|
||||
creationDate: undefined,
|
||||
});
|
||||
accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate });
|
||||
|
||||
const newCreationDate = "2024-06-15T12:30:00.000Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...accountWithoutCreationDate, creationDate: newCreationDate },
|
||||
});
|
||||
});
|
||||
|
||||
it("should update to a different creation date string format", async () => {
|
||||
const newCreationDate = "2023-03-15T08:45:30.123Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo, creationDate: newCreationDate },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAccountVerifyNewDeviceLogin", () => {
|
||||
const initialState = true;
|
||||
beforeEach(() => {
|
||||
@@ -294,6 +365,7 @@ describe("accountService", () => {
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
name: undefined,
|
||||
creationDate: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ const LOGGED_OUT_INFO: AccountInfo = {
|
||||
email: "",
|
||||
emailVerified: false,
|
||||
name: undefined,
|
||||
creationDate: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -167,6 +168,10 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
await this.setAccountInfo(userId, { emailVerified });
|
||||
}
|
||||
|
||||
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
|
||||
await this.setAccountInfo(userId, { creationDate });
|
||||
}
|
||||
|
||||
async clean(userId: UserId) {
|
||||
await this.setAccountInfo(userId, LOGGED_OUT_INFO);
|
||||
await this.removeAccountActivity(userId);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SystemNotificationEvent,
|
||||
SystemNotificationsService,
|
||||
} from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
|
||||
@@ -48,14 +49,16 @@ describe("AuthRequestAnsweringService", () => {
|
||||
|
||||
// Common defaults
|
||||
authService.activeAccountStatus$ = of(AuthenticationStatus.Locked);
|
||||
accountService.activeAccount$ = of({
|
||||
id: userId,
|
||||
const accountInfo = mockAccountInfoWith({
|
||||
email: "user@example.com",
|
||||
emailVerified: true,
|
||||
name: "User",
|
||||
});
|
||||
accountService.activeAccount$ = of({
|
||||
id: userId,
|
||||
...accountInfo,
|
||||
});
|
||||
accountService.accounts$ = of({
|
||||
[userId]: { email: "user@example.com", emailVerified: true, name: "User" },
|
||||
[userId]: accountInfo,
|
||||
});
|
||||
(masterPasswordService.forceSetPasswordReason$ as jest.Mock).mockReturnValue(
|
||||
of(ForceSetPasswordReason.None),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
makeStaticByteArray,
|
||||
mockAccountServiceWith,
|
||||
trackEmissions,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../spec";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
@@ -58,9 +59,10 @@ describe("AuthService", () => {
|
||||
const accountInfo = {
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
id: userId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -112,9 +114,10 @@ describe("AuthService", () => {
|
||||
const accountInfo2 = {
|
||||
status: AuthenticationStatus.Unlocked,
|
||||
id: Utils.newGuid() as UserId,
|
||||
email: "email2",
|
||||
emailVerified: false,
|
||||
name: "name2",
|
||||
...mockAccountInfoWith({
|
||||
email: "email2",
|
||||
name: "name2",
|
||||
}),
|
||||
};
|
||||
|
||||
const emissions = trackEmissions(sut.activeAccountStatus$);
|
||||
@@ -131,11 +134,13 @@ describe("AuthService", () => {
|
||||
it("requests auth status for all known users", async () => {
|
||||
const userId2 = Utils.newGuid() as UserId;
|
||||
|
||||
await accountService.addAccount(userId2, {
|
||||
email: "email2",
|
||||
emailVerified: false,
|
||||
name: "name2",
|
||||
});
|
||||
await accountService.addAccount(
|
||||
userId2,
|
||||
mockAccountInfoWith({
|
||||
email: "email2",
|
||||
name: "name2",
|
||||
}),
|
||||
);
|
||||
|
||||
const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked));
|
||||
sut.authStatusFor$ = mockFn;
|
||||
|
||||
@@ -8,12 +8,13 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../spec/fake-account-service";
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { Account, AccountInfo, AccountService } from "../abstractions/account.service";
|
||||
import { Account, AccountService } from "../abstractions/account.service";
|
||||
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
||||
|
||||
@@ -96,11 +97,10 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
const encryptedKey = "encryptedString";
|
||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||
|
||||
const user1AccountInfo: AccountInfo = {
|
||||
const user1AccountInfo = mockAccountInfoWith({
|
||||
name: "Test User 1",
|
||||
email: "test1@email.com",
|
||||
emailVerified: true,
|
||||
};
|
||||
});
|
||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||
|
||||
keyService.userKey$.mockReturnValue(of({ key: "key" } as any));
|
||||
|
||||
@@ -22,9 +22,7 @@ import { UserKey } from "../../types/key";
|
||||
import { AccountService } from "../abstractions/account.service";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
|
||||
|
||||
export class PasswordResetEnrollmentServiceImplementation
|
||||
implements PasswordResetEnrollmentServiceAbstraction
|
||||
{
|
||||
export class PasswordResetEnrollmentServiceImplementation implements PasswordResetEnrollmentServiceAbstraction {
|
||||
constructor(
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
|
||||
@@ -445,13 +445,15 @@ export class TokenService implements TokenServiceAbstraction {
|
||||
// we can't determine storage location w/out vaultTimeoutAction and vaultTimeout
|
||||
// but we can simply clear all locations to avoid the need to require those parameters.
|
||||
|
||||
// When secure storage is supported, clear the encryption key from secure storage.
|
||||
// When not supported (e.g., portable builds), tokens are stored on disk and this step is skipped.
|
||||
if (this.platformSupportsSecureStorage) {
|
||||
// Always clear the access token key when clearing the access token
|
||||
// The next set of the access token will create a new access token key
|
||||
// Always clear the access token key when clearing the access token.
|
||||
// The next set of the access token will create a new access token key.
|
||||
await this.clearAccessTokenKey(userId);
|
||||
}
|
||||
|
||||
// Platform doesn't support secure storage, so use state provider implementation
|
||||
// Clear tokens from disk storage (all platforms)
|
||||
await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).update((_) => null, {
|
||||
shouldUpdate: (previousValue) => previousValue !== null,
|
||||
});
|
||||
@@ -478,6 +480,9 @@ export class TokenService implements TokenServiceAbstraction {
|
||||
return null;
|
||||
}
|
||||
|
||||
// When platformSupportsSecureStorage=true, tokens on disk are encrypted and require
|
||||
// decryption keys from secure storage. When false (e.g., portable builds), tokens are
|
||||
// stored on disk.
|
||||
if (this.platformSupportsSecureStorage) {
|
||||
let accessTokenKey: AccessTokenKey;
|
||||
try {
|
||||
@@ -1118,6 +1123,9 @@ export class TokenService implements TokenServiceAbstraction {
|
||||
) {
|
||||
return TokenStorageLocation.Memory;
|
||||
} else {
|
||||
// Secure storage (e.g., OS credential manager) is preferred when available.
|
||||
// Desktop portable builds set platformSupportsSecureStorage=false to store tokens
|
||||
// on disk for portability across machines.
|
||||
if (useSecureStorage && this.platformSupportsSecureStorage) {
|
||||
return TokenStorageLocation.SecureStorage;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { PlatformUtilsService } from "../../../platform/abstractions/platform-ut
|
||||
import { OrganizationSponsorshipApiServiceAbstraction } from "../../abstractions/organizations/organization-sponsorship-api.service.abstraction";
|
||||
import { OrganizationSponsorshipInvitesResponse } from "../../models/response/organization-sponsorship-invites.response";
|
||||
|
||||
export class OrganizationSponsorshipApiService
|
||||
implements OrganizationSponsorshipApiServiceAbstraction
|
||||
{
|
||||
export class OrganizationSponsorshipApiService implements OrganizationSponsorshipApiServiceAbstraction {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
|
||||
@@ -43,6 +43,7 @@ export enum FeatureFlag {
|
||||
LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2",
|
||||
UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data",
|
||||
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
|
||||
DataRecoveryTool = "pm-28813-data-recovery-tool",
|
||||
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
|
||||
|
||||
/* Tools */
|
||||
@@ -64,6 +65,7 @@ export enum FeatureFlag {
|
||||
RiskInsightsForPremium = "pm-23904-risk-insights-for-premium",
|
||||
VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders",
|
||||
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
|
||||
MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems",
|
||||
|
||||
/* Platform */
|
||||
IpcChannelFramework = "ipc-channel-framework",
|
||||
@@ -123,6 +125,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.RiskInsightsForPremium]: FALSE,
|
||||
[FeatureFlag.VaultLoadingSkeletons]: FALSE,
|
||||
[FeatureFlag.BrowserPremiumSpotlight]: FALSE,
|
||||
[FeatureFlag.MigrateMyVaultToMyItems]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE,
|
||||
@@ -147,6 +150,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.LinuxBiometricsV2]: FALSE,
|
||||
[FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE,
|
||||
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
|
||||
[FeatureFlag.DataRecoveryTool]: FALSE,
|
||||
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
|
||||
|
||||
/* Platform */
|
||||
|
||||
@@ -91,4 +91,12 @@ export class DefaultKeyGenerationService implements KeyGenerationService {
|
||||
|
||||
return new SymmetricCryptoKey(newKey);
|
||||
}
|
||||
|
||||
async deriveVaultExportKey(
|
||||
password: string,
|
||||
salt: string,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<SymmetricCryptoKey> {
|
||||
return await this.stretchKey(await this.deriveKeyFromPassword(password, salt, kdfConfig));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,4 +87,19 @@ export abstract class KeyGenerationService {
|
||||
* @returns 64 byte derived key.
|
||||
*/
|
||||
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
|
||||
|
||||
/**
|
||||
* Derives a 64 byte key for encrypting and decrypting vault exports.
|
||||
*
|
||||
* @deprecated Do not use this for new use-cases.
|
||||
* @param password Password to derive the key from.
|
||||
* @param salt Salt for the key derivation function.
|
||||
* @param kdfConfig Configuration for the key derivation function.
|
||||
* @returns 64 byte derived key.
|
||||
*/
|
||||
abstract deriveVaultExportKey(
|
||||
password: string,
|
||||
salt: string,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
|
||||
|
||||
export abstract class KeyConnectorApiService {
|
||||
abstract getConfirmationDetails(
|
||||
orgSsoIdentifier: string,
|
||||
): Promise<KeyConnectorConfirmationDetailsResponse>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export interface KeyConnectorDomainConfirmation {
|
||||
keyConnectorUrl: string;
|
||||
organizationSsoIdentifier: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
export class KeyConnectorConfirmationDetailsResponse extends BaseResponse {
|
||||
organizationName: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.organizationName = this.getResponseProperty("OrganizationName");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
|
||||
|
||||
import { DefaultKeyConnectorApiService } from "./default-key-connector-api.service";
|
||||
|
||||
describe("DefaultKeyConnectorApiService", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let sut: DefaultKeyConnectorApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
sut = new DefaultKeyConnectorApiService(apiService);
|
||||
});
|
||||
|
||||
describe("getConfirmationDetails", () => {
|
||||
it("encodes orgSsoIdentifier in URL", async () => {
|
||||
const orgSsoIdentifier = "test org/with special@chars";
|
||||
const expectedEncodedIdentifier = encodeURIComponent(orgSsoIdentifier);
|
||||
const mockResponse = {};
|
||||
apiService.send.mockResolvedValue(mockResponse);
|
||||
|
||||
await sut.getConfirmationDetails(orgSsoIdentifier);
|
||||
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
`/accounts/key-connector/confirmation-details/${expectedEncodedIdentifier}`,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns expected response", async () => {
|
||||
const orgSsoIdentifier = "test-org";
|
||||
const expectedOrgName = "example";
|
||||
const mockResponse = { OrganizationName: expectedOrgName };
|
||||
apiService.send.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await sut.getConfirmationDetails(orgSsoIdentifier);
|
||||
|
||||
expect(result).toBeInstanceOf(KeyConnectorConfirmationDetailsResponse);
|
||||
expect(result.organizationName).toBe(expectedOrgName);
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"GET",
|
||||
"/accounts/key-connector/confirmation-details/test-org",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { KeyConnectorApiService } from "../abstractions/key-connector-api.service";
|
||||
import { KeyConnectorConfirmationDetailsResponse } from "../models/response/key-connector-confirmation-details.response";
|
||||
|
||||
export class DefaultKeyConnectorApiService implements KeyConnectorApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getConfirmationDetails(
|
||||
orgSsoIdentifier: string,
|
||||
): Promise<KeyConnectorConfirmationDetailsResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/accounts/key-connector/confirmation-details/" + encodeURIComponent(orgSsoIdentifier),
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new KeyConnectorConfirmationDetailsResponse(r);
|
||||
}
|
||||
}
|
||||
@@ -603,7 +603,10 @@ describe("KeyConnectorService", () => {
|
||||
const data$ = keyConnectorService.requiresDomainConfirmation$(mockUserId);
|
||||
const data = await firstValueFrom(data$);
|
||||
|
||||
expect(data).toEqual({ keyConnectorUrl: conversion.keyConnectorUrl });
|
||||
expect(data).toEqual({
|
||||
keyConnectorUrl: conversion.keyConnectorUrl,
|
||||
organizationSsoIdentifier: conversion.organizationId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return observable of null value when no data is set", async () => {
|
||||
|
||||
@@ -202,9 +202,16 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
}
|
||||
|
||||
requiresDomainConfirmation$(userId: UserId): Observable<KeyConnectorDomainConfirmation | null> {
|
||||
return this.stateProvider
|
||||
.getUserState$(NEW_SSO_USER_KEY_CONNECTOR_CONVERSION, userId)
|
||||
.pipe(map((data) => (data != null ? { keyConnectorUrl: data.keyConnectorUrl } : null)));
|
||||
return this.stateProvider.getUserState$(NEW_SSO_USER_KEY_CONNECTOR_CONVERSION, userId).pipe(
|
||||
map((data) =>
|
||||
data != null
|
||||
? {
|
||||
keyConnectorUrl: data.keyConnectorUrl,
|
||||
organizationSsoIdentifier: data.organizationId,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private handleKeyConnectorError(e: any) {
|
||||
|
||||
@@ -45,14 +45,6 @@ export abstract class PinStateServiceAbstraction {
|
||||
pinLockType: PinLockType,
|
||||
): Promise<PasswordProtectedKeyEnvelope | null>;
|
||||
|
||||
/**
|
||||
* Gets the user's legacy PIN-protected UserKey
|
||||
* @deprecated Use {@link getPinProtectedUserKeyEnvelope} instead. Only for migration support.
|
||||
* @param userId The user's id
|
||||
* @throws If the user id is not provided
|
||||
*/
|
||||
abstract getLegacyPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString | null>;
|
||||
|
||||
/**
|
||||
* Sets the PIN state for the user
|
||||
* @deprecated - This is not a public API. DO NOT USE IT
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
PIN_PROTECTED_USER_KEY_ENVELOPE_PERSISTENT,
|
||||
PIN_PROTECTED_USER_KEY_ENVELOPE_EPHEMERAL,
|
||||
USER_KEY_ENCRYPTED_PIN,
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
} from "./pin.state";
|
||||
|
||||
export class PinStateService implements PinStateServiceAbstraction {
|
||||
@@ -36,9 +35,7 @@ export class PinStateService implements PinStateServiceAbstraction {
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
const isPersistentPinSet =
|
||||
(await this.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null ||
|
||||
// Deprecated
|
||||
(await this.getLegacyPinKeyEncryptedUserKeyPersistent(userId)) != null;
|
||||
(await this.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null;
|
||||
const isPinSet =
|
||||
(await firstValueFrom(this.stateProvider.getUserState$(USER_KEY_ENCRYPTED_PIN, userId))) !=
|
||||
null;
|
||||
@@ -71,16 +68,6 @@ export class PinStateService implements PinStateServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async getLegacyPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString | null> {
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
return await firstValueFrom(
|
||||
this.stateProvider
|
||||
.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, userId)
|
||||
.pipe(map((value) => (value ? new EncString(value) : null))),
|
||||
);
|
||||
}
|
||||
|
||||
async setPinState(
|
||||
userId: UserId,
|
||||
pinProtectedUserKeyEnvelope: PasswordProtectedKeyEnvelope,
|
||||
@@ -116,9 +103,6 @@ export class PinStateService implements PinStateServiceAbstraction {
|
||||
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, userId);
|
||||
await this.stateProvider.setUserState(PIN_PROTECTED_USER_KEY_ENVELOPE_EPHEMERAL, null, userId);
|
||||
await this.stateProvider.setUserState(PIN_PROTECTED_USER_KEY_ENVELOPE_PERSISTENT, null, userId);
|
||||
|
||||
// Note: This can be deleted after sufficiently many PINs are migrated and the state is removed.
|
||||
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId);
|
||||
}
|
||||
|
||||
async clearEphemeralPinState(userId: UserId): Promise<void> {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
USER_KEY_ENCRYPTED_PIN,
|
||||
PIN_PROTECTED_USER_KEY_ENVELOPE_EPHEMERAL,
|
||||
PIN_PROTECTED_USER_KEY_ENVELOPE_PERSISTENT,
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
} from "./pin.state";
|
||||
|
||||
describe("PinStateService", () => {
|
||||
@@ -121,21 +120,6 @@ describe("PinStateService", () => {
|
||||
expect(result).toBe("PERSISTENT");
|
||||
});
|
||||
|
||||
it("should return 'PERSISTENT' if a legacy pin key encrypted user key (persistent) is found", async () => {
|
||||
// Arrange
|
||||
await stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
mockUserKeyEncryptedPin,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await sut.getPinLockType(mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe("PERSISTENT");
|
||||
});
|
||||
|
||||
it("should return 'EPHEMERAL' if only user key encrypted pin is found", async () => {
|
||||
// Arrange
|
||||
await stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, mockUserKeyEncryptedPin, mockUserId);
|
||||
@@ -164,7 +148,6 @@ describe("PinStateService", () => {
|
||||
null,
|
||||
mockUserId,
|
||||
);
|
||||
await stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, mockUserId);
|
||||
await stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, mockUserId);
|
||||
|
||||
// Act
|
||||
@@ -290,45 +273,6 @@ describe("PinStateService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLegacyPinKeyEncryptedUserKeyPersistent()", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test.each([null, undefined])("throws if userId is %p", async (userId) => {
|
||||
// Act & Assert
|
||||
await expect(() =>
|
||||
sut.getLegacyPinKeyEncryptedUserKeyPersistent(userId as any),
|
||||
).rejects.toThrow("userId is null or undefined.");
|
||||
});
|
||||
|
||||
it("should return EncString when legacy key is set", async () => {
|
||||
// Arrange
|
||||
await stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
mockUserKeyEncryptedPin,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(result?.encryptedString).toEqual(mockUserKeyEncryptedPin);
|
||||
});
|
||||
|
||||
test.each([null, undefined])("should return null when legacy key is %p", async (value) => {
|
||||
// Arrange
|
||||
await stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, value, mockUserId);
|
||||
|
||||
// Act
|
||||
const result = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPinState()", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -464,22 +408,6 @@ describe("PinStateService", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("clears legacy PIN key encrypted user key persistent", async () => {
|
||||
// Arrange
|
||||
await stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
mockUserKeyEncryptedPin,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// Act
|
||||
await sut.clearPinState(mockUserId);
|
||||
|
||||
// Assert
|
||||
const result = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("clears all PIN state when all types are set", async () => {
|
||||
// Arrange - set up all possible PIN state
|
||||
await sut.setPinState(
|
||||
@@ -494,17 +422,11 @@ describe("PinStateService", () => {
|
||||
mockUserKeyEncryptedPin,
|
||||
"EPHEMERAL",
|
||||
);
|
||||
await stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
mockUserKeyEncryptedPin,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// Verify all state is set before clearing
|
||||
expect(await firstValueFrom(sut.userKeyEncryptedPin$(mockUserId))).not.toBeNull();
|
||||
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL")).not.toBeNull();
|
||||
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "PERSISTENT")).not.toBeNull();
|
||||
expect(await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId)).not.toBeNull();
|
||||
|
||||
// Act
|
||||
await sut.clearPinState(mockUserId);
|
||||
@@ -513,7 +435,6 @@ describe("PinStateService", () => {
|
||||
expect(await firstValueFrom(sut.userKeyEncryptedPin$(mockUserId))).toBeNull();
|
||||
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL")).toBeNull();
|
||||
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "PERSISTENT")).toBeNull();
|
||||
expect(await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId)).toBeNull();
|
||||
});
|
||||
|
||||
it("results in PIN lock type DISABLED after clearing", async () => {
|
||||
@@ -545,7 +466,6 @@ describe("PinStateService", () => {
|
||||
expect(await firstValueFrom(sut.userKeyEncryptedPin$(mockUserId))).toBeNull();
|
||||
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL")).toBeNull();
|
||||
expect(await sut.getPinProtectedUserKeyEnvelope(mockUserId, "PERSISTENT")).toBeNull();
|
||||
expect(await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId)).toBeNull();
|
||||
expect(await sut.getPinLockType(mockUserId)).toBe("DISABLED");
|
||||
});
|
||||
});
|
||||
@@ -623,32 +543,6 @@ describe("PinStateService", () => {
|
||||
expect(ephemeralResult).toBeNull();
|
||||
});
|
||||
|
||||
it("does not clear legacy PIN key encrypted user key persistent", async () => {
|
||||
// Arrange - set up ephemeral state and legacy state
|
||||
await sut.setPinState(
|
||||
mockUserId,
|
||||
mockEphemeralEnvelope,
|
||||
mockUserKeyEncryptedPin,
|
||||
"EPHEMERAL",
|
||||
);
|
||||
await stateProvider.setUserState(
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
mockUserKeyEncryptedPin,
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
// Act
|
||||
await sut.clearEphemeralPinState(mockUserId);
|
||||
|
||||
// Assert - legacy PIN should still be present
|
||||
const legacyResult = await sut.getLegacyPinKeyEncryptedUserKeyPersistent(mockUserId);
|
||||
expect(legacyResult?.encryptedString).toEqual(mockUserKeyEncryptedPin);
|
||||
|
||||
// Assert - ephemeral envelope should be cleared
|
||||
const ephemeralResult = await sut.getPinProtectedUserKeyEnvelope(mockUserId, "EPHEMERAL");
|
||||
expect(ephemeralResult).toBeNull();
|
||||
});
|
||||
|
||||
it("changes PIN lock type from EPHEMERAL to DISABLED when no other PIN state exists", async () => {
|
||||
// Arrange - set up only ephemeral PIN state
|
||||
await sut.setPinState(
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
import { PinKey, UserKey } from "../../types/key";
|
||||
import { UserKey } from "../../types/key";
|
||||
|
||||
import { PinLockType } from "./pin-lock-type";
|
||||
|
||||
@@ -69,10 +66,4 @@ export abstract class PinServiceAbstraction {
|
||||
* @deprecated This is not deprecated, but only meant to be called by KeyService. DO NOT USE IT.
|
||||
*/
|
||||
abstract userUnlocked(userId: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
* Makes a PinKey from the provided PIN.
|
||||
* @deprecated - Note: This is currently re-used by vault exports, which is still permitted but should be refactored out to use a different construct.
|
||||
*/
|
||||
abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { assertNonNullish } from "../../auth/utils";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { PinKey, UserKey } from "../../types/key";
|
||||
import { KeyGenerationService } from "../crypto";
|
||||
import { UserKey } from "../../types/key";
|
||||
import { firstValueFromOrThrow } from "../utils";
|
||||
|
||||
import { PinLockType } from "./pin-lock-type";
|
||||
@@ -21,10 +18,7 @@ import { PinServiceAbstraction } from "./pin.service.abstraction";
|
||||
|
||||
export class PinService implements PinServiceAbstraction {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private encryptService: EncryptService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private logService: LogService,
|
||||
private keyService: KeyService,
|
||||
private sdkService: SdkService,
|
||||
@@ -56,19 +50,6 @@ export class PinService implements PinServiceAbstraction {
|
||||
// On first unlock, set the ephemeral pin envelope, if it is not set yet
|
||||
const pin = await this.getPin(userId);
|
||||
await this.setPin(pin, "EPHEMERAL", userId);
|
||||
} else if ((await this.pinStateService.getPinLockType(userId)) === "PERSISTENT") {
|
||||
// Encrypted migration for persistent pin unlock to pin envelopes.
|
||||
// This will be removed at the earliest in 2026.1.0
|
||||
//
|
||||
// ----- ENCRYPTION MIGRATION -----
|
||||
// Pin-key encrypted user-keys are eagerly migrated to the new pin-protected user key envelope format.
|
||||
if ((await this.pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent(userId)) != null) {
|
||||
this.logService.info(
|
||||
"[Pin Service] Migrating legacy PIN key to PinProtectedUserKeyEnvelope",
|
||||
);
|
||||
const pin = await this.getPin(userId);
|
||||
await this.setPin(pin, "PERSISTENT", userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,86 +125,30 @@ export class PinService implements PinServiceAbstraction {
|
||||
assertNonNullish(pin, "pin");
|
||||
assertNonNullish(userId, "userId");
|
||||
|
||||
const hasPinProtectedKeyEnvelopeSet =
|
||||
(await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "EPHEMERAL")) != null ||
|
||||
(await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, "PERSISTENT")) != null;
|
||||
this.logService.info("[Pin Service] Pin-unlock via PinProtectedUserKeyEnvelope");
|
||||
|
||||
if (hasPinProtectedKeyEnvelopeSet) {
|
||||
this.logService.info("[Pin Service] Pin-unlock via PinProtectedUserKeyEnvelope");
|
||||
const pinLockType = await this.pinStateService.getPinLockType(userId);
|
||||
const envelope = await this.pinStateService.getPinProtectedUserKeyEnvelope(userId, pinLockType);
|
||||
|
||||
const pinLockType = await this.pinStateService.getPinLockType(userId);
|
||||
const envelope = await this.pinStateService.getPinProtectedUserKeyEnvelope(
|
||||
userId,
|
||||
pinLockType,
|
||||
try {
|
||||
// Use the sdk to create an enrollment, not yet persisting it to state
|
||||
const startTime = performance.now();
|
||||
const userKeyBytes = await firstValueFrom(
|
||||
this.sdkService.client$.pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
return sdk.crypto().unseal_password_protected_key_envelope(pin, envelope!);
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.logService.measure(startTime, "Crypto", "PinService", "UnsealPinEnvelope");
|
||||
|
||||
try {
|
||||
// Use the sdk to create an enrollment, not yet persisting it to state
|
||||
const startTime = performance.now();
|
||||
const userKeyBytes = await firstValueFrom(
|
||||
this.sdkService.client$.pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
return sdk.crypto().unseal_password_protected_key_envelope(pin, envelope!);
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.logService.measure(startTime, "Crypto", "PinService", "UnsealPinEnvelope");
|
||||
|
||||
return new SymmetricCryptoKey(userKeyBytes) as UserKey;
|
||||
} catch (error) {
|
||||
this.logService.error(`Failed to unseal pin: ${error}`);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
this.logService.info("[Pin Service] Pin-unlock via legacy PinKeyEncryptedUserKey");
|
||||
|
||||
// This branch is deprecated and will be removed in the future, but is kept for migration.
|
||||
try {
|
||||
const pinKeyEncryptedUserKey =
|
||||
await this.pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent(userId);
|
||||
const email = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return await this.decryptUserKey(pin, email, kdfConfig, pinKeyEncryptedUserKey!);
|
||||
} catch (error) {
|
||||
this.logService.error(`Error decrypting user key with pin: ${error}`);
|
||||
return null;
|
||||
}
|
||||
return new SymmetricCryptoKey(userKeyBytes) as UserKey;
|
||||
} catch (error) {
|
||||
this.logService.error(`Failed to unseal pin: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything below here is deprecated and will be removed subsequently
|
||||
|
||||
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||
const startTime = performance.now();
|
||||
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
|
||||
this.logService.measure(startTime, "Crypto", "PinService", "makePinKey");
|
||||
|
||||
return (await this.keyGenerationService.stretchKey(pinKey)) as PinKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the UserKey with the provided PIN.
|
||||
* @deprecated
|
||||
* @throws If the PIN does not match the PIN that was used to encrypt the user key
|
||||
* @throws If the salt, or KDF don't match the salt / KDF used to encrypt the user key
|
||||
*/
|
||||
private async decryptUserKey(
|
||||
pin: string,
|
||||
salt: string,
|
||||
kdfConfig: KdfConfig,
|
||||
pinKeyEncryptedUserKey: EncString,
|
||||
): Promise<UserKey> {
|
||||
assertNonNullish(pin, "pin");
|
||||
assertNonNullish(salt, "salt");
|
||||
assertNonNullish(kdfConfig, "kdfConfig");
|
||||
assertNonNullish(pinKeyEncryptedUserKey, "pinKeyEncryptedUserKey");
|
||||
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
||||
const userKey = await this.encryptService.unwrapSymmetricKey(pinKeyEncryptedUserKey, pinKey);
|
||||
return userKey as UserKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@ import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, filter } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordProtectedKeyEnvelope } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { MockSdkService } from "../..//platform/spec/mock-sdk.service";
|
||||
import { FakeAccountService, mockAccountServiceWith, mockEnc } from "../../../spec";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { PinKey, UserKey } from "../../types/key";
|
||||
import { KeyGenerationService } from "../crypto";
|
||||
import { UserKey } from "../../types/key";
|
||||
import { EncryptService } from "../crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString, EncString } from "../crypto/models/enc-string";
|
||||
|
||||
@@ -22,16 +20,10 @@ import { PinService } from "./pin.service.implementation";
|
||||
describe("PinService", () => {
|
||||
let sut: PinService;
|
||||
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const encryptService = mock<EncryptService>();
|
||||
const kdfConfigService = mock<KdfConfigService>();
|
||||
const keyGenerationService = mock<KeyGenerationService>();
|
||||
const logService = mock<LogService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockPinKey = new SymmetricCryptoKey(randomBytes(32)) as PinKey;
|
||||
const mockUserEmail = "user@example.com";
|
||||
const mockPin = "1234";
|
||||
const mockUserKeyEncryptedPin = new EncString("userKeyEncryptedPin");
|
||||
const mockEphemeralEnvelope = "mock-ephemeral-envelope" as PasswordProtectedKeyEnvelope;
|
||||
@@ -42,7 +34,6 @@ describe("PinService", () => {
|
||||
const behaviorSubject = new BehaviorSubject<{ userId: UserId; userKey: UserKey }>(null);
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
|
||||
(keyService as any)["unlockedUserKeys$"] = behaviorSubject
|
||||
.asObservable()
|
||||
.pipe(filter((x) => x != null));
|
||||
@@ -50,16 +41,7 @@ describe("PinService", () => {
|
||||
.mockDeep()
|
||||
.unseal_password_protected_key_envelope.mockReturnValue(new Uint8Array(64));
|
||||
|
||||
sut = new PinService(
|
||||
accountService,
|
||||
encryptService,
|
||||
kdfConfigService,
|
||||
keyGenerationService,
|
||||
logService,
|
||||
keyService,
|
||||
sdkService,
|
||||
pinStateService,
|
||||
);
|
||||
sut = new PinService(encryptService, logService, keyService, sdkService, pinStateService);
|
||||
});
|
||||
|
||||
it("should instantiate the PinService", () => {
|
||||
@@ -89,26 +71,6 @@ describe("PinService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should migrate legacy persistent PIN if needed", async () => {
|
||||
// Arrange
|
||||
pinStateService.getPinLockType.mockResolvedValue("PERSISTENT");
|
||||
pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
|
||||
mockEnc("legacy-key"),
|
||||
);
|
||||
const getPinSpy = jest.spyOn(sut, "getPin").mockResolvedValue(mockPin);
|
||||
const setPinSpy = jest.spyOn(sut, "setPin").mockResolvedValue();
|
||||
|
||||
// Act
|
||||
await sut.userUnlocked(mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(getPinSpy).toHaveBeenCalledWith(mockUserId);
|
||||
expect(setPinSpy).toHaveBeenCalledWith(mockPin, "PERSISTENT", mockUserId);
|
||||
expect(logService.info).toHaveBeenCalledWith(
|
||||
"[Pin Service] Migrating legacy PIN key to PinProtectedUserKeyEnvelope",
|
||||
);
|
||||
});
|
||||
|
||||
it("should do nothing if no migration or setup is needed", async () => {
|
||||
// Arrange
|
||||
pinStateService.getPinLockType.mockResolvedValue("DISABLED");
|
||||
@@ -124,28 +86,6 @@ describe("PinService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("makePinKey()", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should make a PinKey", async () => {
|
||||
// Arrange
|
||||
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(mockPinKey);
|
||||
|
||||
// Act
|
||||
await sut.makePinKey(mockPin, mockUserEmail, DEFAULT_KDF_CONFIG);
|
||||
|
||||
// Assert
|
||||
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
|
||||
mockPin,
|
||||
mockUserEmail,
|
||||
DEFAULT_KDF_CONFIG,
|
||||
);
|
||||
expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(mockPinKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPin()", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -383,7 +323,6 @@ describe("PinService", () => {
|
||||
jest.clearAllMocks();
|
||||
pinStateService.userKeyEncryptedPin$.mockReset();
|
||||
pinStateService.getPinProtectedUserKeyEnvelope.mockReset();
|
||||
pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent.mockReset();
|
||||
});
|
||||
|
||||
it("should throw an error if userId is null", async () => {
|
||||
@@ -423,32 +362,5 @@ describe("PinService", () => {
|
||||
// Assert
|
||||
expect(result).toEqual(mockUserKey);
|
||||
});
|
||||
|
||||
it("should return userkey with legacy pin PERSISTENT", async () => {
|
||||
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(mockPinKey);
|
||||
keyGenerationService.stretchKey.mockResolvedValue(mockPinKey);
|
||||
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
|
||||
encryptService.unwrapSymmetricKey.mockResolvedValue(mockUserKey);
|
||||
|
||||
// Arrange
|
||||
const mockPin = "1234";
|
||||
pinStateService.userKeyEncryptedPin$.mockReturnValueOnce(
|
||||
new BehaviorSubject(mockUserKeyEncryptedPin),
|
||||
);
|
||||
pinStateService.getLegacyPinKeyEncryptedUserKeyPersistent.mockResolvedValueOnce(
|
||||
mockUserKeyEncryptedPin,
|
||||
);
|
||||
|
||||
// Act
|
||||
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockUserKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test helpers
|
||||
function randomBytes(length: number): Uint8Array {
|
||||
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
|
||||
}
|
||||
|
||||
@@ -3,22 +3,6 @@ import { PasswordProtectedKeyEnvelope } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { EncryptedString } from "../crypto/models/enc-string";
|
||||
|
||||
/**
|
||||
* The persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||
*
|
||||
* @deprecated
|
||||
* @remarks Persists through a client reset. Used when `requireMasterPasswordOnClientRestart` is disabled.
|
||||
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
|
||||
*/
|
||||
export const PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT = new UserKeyDefinition<EncryptedString>(
|
||||
PIN_DISK,
|
||||
"pinKeyEncryptedUserKeyPersistent",
|
||||
{
|
||||
deserializer: (jsonValue) => jsonValue,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The persistent (stored on disk) version of the UserKey, stored in a `PasswordProtectedKeyEnvelope`.
|
||||
*
|
||||
|
||||
@@ -56,7 +56,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock.
|
||||
// If there is an active user, check if they have an ephemeral PIN. If so, prevent process reload upon lock.
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
if (userId != null) {
|
||||
if ((await this.pinService.getPinLockType(userId)) === "EPHEMERAL") {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BehaviorSubject, from, of } from "rxjs";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LockService, LogoutService } from "@bitwarden/auth/common";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { FakeAccountService, mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -109,19 +109,19 @@ describe("VaultTimeoutService", () => {
|
||||
if (globalSetups?.userId) {
|
||||
accountService.activeAccountSubject.next({
|
||||
id: globalSetups.userId as UserId,
|
||||
email: null,
|
||||
emailVerified: false,
|
||||
name: null,
|
||||
...mockAccountInfoWith({
|
||||
email: null,
|
||||
name: null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
accountService.accounts$ = of(
|
||||
Object.entries(accounts).reduce(
|
||||
(agg, [id]) => {
|
||||
agg[id] = {
|
||||
agg[id] = mockAccountInfoWith({
|
||||
email: "",
|
||||
emailVerified: true,
|
||||
name: "",
|
||||
};
|
||||
});
|
||||
return agg;
|
||||
},
|
||||
{} as Record<string, AccountInfo>,
|
||||
|
||||
@@ -5,8 +5,10 @@ export interface IpcMessage {
|
||||
message: SerializedOutgoingMessage;
|
||||
}
|
||||
|
||||
export interface SerializedOutgoingMessage
|
||||
extends Omit<OutgoingMessage, typeof Symbol.dispose | "free" | "payload"> {
|
||||
export interface SerializedOutgoingMessage extends Omit<
|
||||
OutgoingMessage,
|
||||
typeof Symbol.dispose | "free" | "payload"
|
||||
> {
|
||||
payload: number[];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../../spec";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||
@@ -163,9 +164,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
} else {
|
||||
activeUserAccount$.next({
|
||||
id: userId,
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
emailVerified: true,
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -174,7 +176,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
const currentAccounts = (userAccounts$.getValue() as Record<string, any>) ?? {};
|
||||
userAccounts$.next({
|
||||
...currentAccounts,
|
||||
[userId]: { email: "email", name: "Test Name", emailVerified: true },
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
}),
|
||||
} as any);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
|
||||
import { awaitAsync } from "../../../../spec";
|
||||
import { awaitAsync, mockAccountInfoWith } from "../../../../spec";
|
||||
import { Matrix } from "../../../../spec/matrix";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
@@ -139,11 +139,18 @@ describe("NotificationsService", () => {
|
||||
activeAccount.next(null);
|
||||
accounts.next({} as any);
|
||||
} else {
|
||||
activeAccount.next({ id: userId, email: "email", name: "Test Name", emailVerified: true });
|
||||
const accountInfo = mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
});
|
||||
activeAccount.next({
|
||||
id: userId,
|
||||
...accountInfo,
|
||||
});
|
||||
const current = (accounts.getValue() as Record<string, any>) ?? {};
|
||||
accounts.next({
|
||||
...current,
|
||||
[userId]: { email: "email", name: "Test Name", emailVerified: true },
|
||||
[userId]: accountInfo,
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
@@ -349,7 +356,13 @@ describe("NotificationsService", () => {
|
||||
describe("processNotification", () => {
|
||||
beforeEach(async () => {
|
||||
appIdService.getAppId.mockResolvedValue("test-app-id");
|
||||
activeAccount.next({ id: mockUser1, email: "email", name: "Test Name", emailVerified: true });
|
||||
activeAccount.next({
|
||||
id: mockUser1,
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "Test Name",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationType.LogOut", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, awaitAsync } from "../../../spec";
|
||||
import { FakeStateProvider, awaitAsync, mockAccountInfoWith } from "../../../spec";
|
||||
import { FakeAccountService } from "../../../spec/fake-account-service";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CloudRegion, Region } from "../abstractions/environment.service";
|
||||
@@ -28,16 +28,14 @@ describe("EnvironmentService", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = new FakeAccountService({
|
||||
[testUser]: {
|
||||
[testUser]: mockAccountInfoWith({
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
},
|
||||
[alternateTestUser]: {
|
||||
}),
|
||||
[alternateTestUser]: mockAccountInfoWith({
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
@@ -47,9 +45,10 @@ describe("EnvironmentService", () => {
|
||||
const switchUser = async (userId: UserId) => {
|
||||
accountService.activeAccountSubject.next({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
emailVerified: false,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: `Test Name ${userId}`,
|
||||
}),
|
||||
});
|
||||
await awaitAsync();
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TextEncoder } from "util";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { mockAccountServiceWith } from "../../../../spec";
|
||||
import { mockAccountServiceWith, mockAccountInfoWith } from "../../../../spec";
|
||||
import { Account } from "../../../auth/abstractions/account.service";
|
||||
import { CipherId, UserId } from "../../../types/guid";
|
||||
import { CipherService, EncryptionContext } from "../../../vault/abstractions/cipher.service";
|
||||
@@ -40,9 +40,10 @@ describe("FidoAuthenticatorService", () => {
|
||||
const userId = "testId" as UserId;
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: userId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
let cipherService!: MockProxy<CipherService>;
|
||||
|
||||
@@ -46,9 +46,9 @@ const KeyUsages: KeyUsage[] = ["sign"];
|
||||
*
|
||||
* It is highly recommended that the W3C specification is used a reference when reading this code.
|
||||
*/
|
||||
export class Fido2AuthenticatorService<ParentWindowReference>
|
||||
implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference>
|
||||
{
|
||||
export class Fido2AuthenticatorService<
|
||||
ParentWindowReference,
|
||||
> implements Fido2AuthenticatorServiceAbstraction<ParentWindowReference> {
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private userInterface: Fido2UserInterfaceService<ParentWindowReference>,
|
||||
|
||||
@@ -47,9 +47,9 @@ import { guidToRawFormat } from "./guid-utils";
|
||||
*
|
||||
* It is highly recommended that the W3C specification is used a reference when reading this code.
|
||||
*/
|
||||
export class Fido2ClientService<ParentWindowReference>
|
||||
implements Fido2ClientServiceAbstraction<ParentWindowReference>
|
||||
{
|
||||
export class Fido2ClientService<
|
||||
ParentWindowReference,
|
||||
> implements Fido2ClientServiceAbstraction<ParentWindowReference> {
|
||||
private timeoutAbortController: AbortController;
|
||||
private readonly TIMEOUTS = {
|
||||
NO_VERIFICATION: {
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { EncryptedString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
@@ -92,7 +92,10 @@ describe("DefaultSdkService", () => {
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
|
||||
accountService.accounts$ = of({
|
||||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
kdfConfigService.getKdfConfig$
|
||||
.calledWith(userId)
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { AccountInfo } from "../../../auth/abstractions/account.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { ConfigService } from "../../abstractions/config/config.service";
|
||||
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
|
||||
@@ -76,7 +76,10 @@ describe("DefaultRegisterSdkService", () => {
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(new BehaviorSubject(mock<Environment>()));
|
||||
accountService.accounts$ = of({
|
||||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,7 +128,10 @@ describe("DefaultRegisterSdkService", () => {
|
||||
|
||||
it("destroys the internal SDK client when the account is removed (logout)", async () => {
|
||||
const accounts$ = new BehaviorSubject({
|
||||
[userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo,
|
||||
[userId]: mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
accountService.accounts$ = accounts$;
|
||||
|
||||
|
||||
@@ -272,6 +272,7 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
|
||||
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
|
||||
await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices);
|
||||
await this.accountService.setAccountCreationDate(response.id, response.creationDate);
|
||||
|
||||
await this.billingAccountProfileStateService.setHasPremium(
|
||||
response.premiumPersonally,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ObservedValueOf, of } from "rxjs";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { mockAccountInfoWith } from "../../spec";
|
||||
import { AccountService } from "../auth/abstractions/account.service";
|
||||
import { TokenService } from "../auth/abstractions/token.service";
|
||||
import { DeviceType } from "../enums";
|
||||
@@ -55,9 +56,10 @@ describe("ApiService", () => {
|
||||
|
||||
accountService.activeAccount$ = of({
|
||||
id: testActiveUser,
|
||||
email: "user1@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test Name",
|
||||
...mockAccountInfoWith({
|
||||
email: "user1@example.com",
|
||||
name: "Test Name",
|
||||
}),
|
||||
} satisfies ObservedValueOf<AccountService["activeAccount$"]>);
|
||||
|
||||
httpOperations = mock();
|
||||
|
||||
@@ -63,6 +63,7 @@ import { UpdateProfileRequest } from "../auth/models/request/update-profile.requ
|
||||
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentitySsoRequiredResponse } from "../auth/models/response/identity-sso-required.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
@@ -165,7 +166,10 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
): Promise<
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
| IdentitySsoRequiredResponse
|
||||
> {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
@@ -212,6 +216,8 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE
|
||||
) {
|
||||
return new IdentityDeviceVerificationResponse(responseJson);
|
||||
} else if (response.status === 400 && responseJson?.SsoOrganizationIdentifier) {
|
||||
return new IdentitySsoRequiredResponse(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, awaitAsync } from "../../../spec";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
awaitAsync,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../spec";
|
||||
import { Account } from "../../auth/abstractions/account.service";
|
||||
import { EXTENSION_DISK, UserKeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
@@ -21,9 +26,10 @@ import { SimpleLogin } from "./vendor/simplelogin";
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomeAccount = {
|
||||
id: SomeUser,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FakeStateProvider,
|
||||
awaitAsync,
|
||||
mockAccountServiceWith,
|
||||
mockAccountInfoWith,
|
||||
} from "../../../../spec";
|
||||
import { KeyGenerationService } from "../../../key-management/crypto";
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -71,9 +72,10 @@ describe("SendService", () => {
|
||||
|
||||
accountService.activeAccountSubject.next({
|
||||
id: mockUserId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
...mockAccountInfoWith({
|
||||
email: "email",
|
||||
name: "name",
|
||||
}),
|
||||
});
|
||||
|
||||
// Initial encrypted state
|
||||
|
||||
@@ -14,9 +14,11 @@ import { Classifier } from "./classifier";
|
||||
* Data that cannot be serialized by JSON.stringify() should
|
||||
* be excluded.
|
||||
*/
|
||||
export class SecretClassifier<Plaintext extends object, Disclosed, Secret>
|
||||
implements Classifier<Plaintext, Disclosed, Secret>
|
||||
{
|
||||
export class SecretClassifier<Plaintext extends object, Disclosed, Secret> implements Classifier<
|
||||
Plaintext,
|
||||
Disclosed,
|
||||
Secret
|
||||
> {
|
||||
private constructor(
|
||||
disclosed: readonly (keyof Jsonify<Disclosed> & keyof Jsonify<Plaintext>)[],
|
||||
excluded: readonly (keyof Plaintext)[],
|
||||
|
||||
@@ -25,9 +25,13 @@ const ONE_MINUTE = 1000 * 60;
|
||||
*
|
||||
* DO NOT USE THIS for synchronized data.
|
||||
*/
|
||||
export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret>
|
||||
implements SingleUserState<Outer>
|
||||
{
|
||||
export class SecretState<
|
||||
Outer,
|
||||
Id,
|
||||
Plaintext extends object,
|
||||
Disclosed,
|
||||
Secret,
|
||||
> implements SingleUserState<Outer> {
|
||||
// The constructor is private to avoid creating a circular dependency when
|
||||
// wiring the derived and secret states together.
|
||||
private constructor(
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
awaitAsync,
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountInfoWith,
|
||||
ObservableTracker,
|
||||
} from "../../../spec";
|
||||
import { Account } from "../../auth/abstractions/account.service";
|
||||
@@ -23,17 +24,19 @@ import { UserStateSubject } from "./user-state-subject";
|
||||
const SomeUser = "some user" as UserId;
|
||||
const SomeAccount = {
|
||||
id: SomeUser,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
const SomeAccount$ = new BehaviorSubject<Account>(SomeAccount);
|
||||
|
||||
const SomeOtherAccount = {
|
||||
id: "some other user" as UserId,
|
||||
email: "someone@example.com",
|
||||
emailVerified: true,
|
||||
name: "Someone",
|
||||
...mockAccountInfoWith({
|
||||
email: "someone@example.com",
|
||||
name: "Someone",
|
||||
}),
|
||||
};
|
||||
|
||||
type TestType = { foo: string };
|
||||
|
||||
@@ -79,11 +79,11 @@ const DEFAULT_FRAME_SIZE = 32;
|
||||
* @template Dependencies use-specific dependencies provided by the user.
|
||||
*/
|
||||
export class UserStateSubject<
|
||||
State extends object,
|
||||
Secret = State,
|
||||
Disclosed = Record<string, never>,
|
||||
Dependencies = null,
|
||||
>
|
||||
State extends object,
|
||||
Secret = State,
|
||||
Disclosed = Record<string, never>,
|
||||
Dependencies = null,
|
||||
>
|
||||
extends Observable<State>
|
||||
implements SubjectLike<State>
|
||||
{
|
||||
|
||||
@@ -9,8 +9,6 @@ export type PrfKey = Opaque<SymmetricCryptoKey, "PrfKey">;
|
||||
export type UserKey = Opaque<SymmetricCryptoKey, "UserKey">;
|
||||
/** @deprecated Interacting with the master key directly is prohibited. Use a high level function from MasterPasswordService instead. */
|
||||
export type MasterKey = Opaque<SymmetricCryptoKey, "MasterKey">;
|
||||
/** @deprecated */
|
||||
export type PinKey = Opaque<SymmetricCryptoKey, "PinKey">;
|
||||
export type OrgKey = Opaque<SymmetricCryptoKey, "OrgKey">;
|
||||
export type ProviderKey = Opaque<SymmetricCryptoKey, "ProviderKey">;
|
||||
export type CipherKey = Opaque<SymmetricCryptoKey, "CipherKey">;
|
||||
|
||||
@@ -13,18 +13,19 @@ describe("buildCipherIcon", () => {
|
||||
},
|
||||
} as any as CipherView;
|
||||
|
||||
it.each([true, false])("handles android app URIs for showFavicon setting %s", (showFavicon) => {
|
||||
setUri("androidapp://test.example");
|
||||
// @TODO Uncomment once we have Android and iOS icons https://bitwarden.atlassian.net/browse/PM-29028
|
||||
// it.each([true, false])("handles android app URIs for showFavicon setting %s", (showFavicon) => {
|
||||
// setUri("androidapp://test.example");
|
||||
|
||||
const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
|
||||
// const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
|
||||
|
||||
expect(iconDetails).toEqual({
|
||||
icon: "bwi-android",
|
||||
image: null,
|
||||
fallbackImage: "",
|
||||
imageEnabled: showFavicon,
|
||||
});
|
||||
});
|
||||
// expect(iconDetails).toEqual({
|
||||
// icon: "bwi-android",
|
||||
// image: null,
|
||||
// fallbackImage: "",
|
||||
// imageEnabled: showFavicon,
|
||||
// });
|
||||
// });
|
||||
|
||||
it("does not mark as an android app if the protocol is not androidapp", () => {
|
||||
// This weird URI points to test.androidapp with a default port and path of /.example
|
||||
@@ -40,18 +41,18 @@ describe("buildCipherIcon", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([true, false])("handles ios app URIs for showFavicon setting %s", (showFavicon) => {
|
||||
setUri("iosapp://test.example");
|
||||
// @TODO Uncomment once we have Android and iOS icons https://bitwarden.atlassian.net/browse/PM-29028
|
||||
// it.each([true, false])("handles ios app URIs for showFavicon setting %s", (showFavicon) => {
|
||||
// setUri("iosapp://test.example");
|
||||
|
||||
const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
|
||||
|
||||
expect(iconDetails).toEqual({
|
||||
icon: "bwi-apple",
|
||||
image: null,
|
||||
fallbackImage: "",
|
||||
imageEnabled: showFavicon,
|
||||
});
|
||||
});
|
||||
// const iconDetails = buildCipherIcon(iconServerUrl, cipher, showFavicon);
|
||||
// expect(iconDetails).toEqual({
|
||||
// icon: "bwi-apple",
|
||||
// image: null,
|
||||
// fallbackImage: "",
|
||||
// imageEnabled: showFavicon,
|
||||
// });
|
||||
// });
|
||||
|
||||
it("does not mark as an ios app if the protocol is not iosapp", () => {
|
||||
// This weird URI points to test.iosapp with a default port and path of /.example
|
||||
|
||||
@@ -49,10 +49,12 @@ export function buildCipherIcon(
|
||||
let isWebsite = false;
|
||||
|
||||
if (hostnameUri.indexOf("androidapp://") === 0) {
|
||||
icon = "bwi-android";
|
||||
// @TODO Re-add once we have Android icon https://bitwarden.atlassian.net/browse/PM-29028
|
||||
// icon = "bwi-android";
|
||||
image = null;
|
||||
} else if (hostnameUri.indexOf("iosapp://") === 0) {
|
||||
icon = "bwi-apple";
|
||||
// @TODO Re-add once we have iOS icon https://bitwarden.atlassian.net/browse/PM-29028
|
||||
// icon = "bwi-apple";
|
||||
image = null;
|
||||
} else if (
|
||||
showFavicon &&
|
||||
|
||||
@@ -155,7 +155,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
if (this.login != null) {
|
||||
model.login = await this.login.decrypt(
|
||||
bypassValidation,
|
||||
userKeyOrOrgKey,
|
||||
cipherDecryptionKey,
|
||||
`Cipher Id: ${this.id}`,
|
||||
);
|
||||
}
|
||||
@@ -167,17 +167,20 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
break;
|
||||
case CipherType.Card:
|
||||
if (this.card != null) {
|
||||
model.card = await this.card.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
|
||||
model.card = await this.card.decrypt(cipherDecryptionKey, `Cipher Id: ${this.id}`);
|
||||
}
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
if (this.identity != null) {
|
||||
model.identity = await this.identity.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
|
||||
model.identity = await this.identity.decrypt(
|
||||
cipherDecryptionKey,
|
||||
`Cipher Id: ${this.id}`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
if (this.sshKey != null) {
|
||||
model.sshKey = await this.sshKey.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
|
||||
model.sshKey = await this.sshKey.decrypt(cipherDecryptionKey, `Cipher Id: ${this.id}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -188,7 +191,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
const attachments: AttachmentView[] = [];
|
||||
for (const attachment of this.attachments) {
|
||||
const decryptedAttachment = await attachment.decrypt(
|
||||
userKeyOrOrgKey,
|
||||
cipherDecryptionKey,
|
||||
`Cipher Id: ${this.id}`,
|
||||
);
|
||||
attachments.push(decryptedAttachment);
|
||||
@@ -199,7 +202,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
if (this.fields != null && this.fields.length > 0) {
|
||||
const fields: FieldView[] = [];
|
||||
for (const field of this.fields) {
|
||||
const decryptedField = await field.decrypt(userKeyOrOrgKey);
|
||||
const decryptedField = await field.decrypt(cipherDecryptionKey);
|
||||
fields.push(decryptedField);
|
||||
}
|
||||
model.fields = fields;
|
||||
@@ -208,7 +211,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
if (this.passwordHistory != null && this.passwordHistory.length > 0) {
|
||||
const passwordHistory: PasswordHistoryView[] = [];
|
||||
for (const ph of this.passwordHistory) {
|
||||
const decryptedPh = await ph.decrypt(userKeyOrOrgKey);
|
||||
const decryptedPh = await ph.decrypt(cipherDecryptionKey);
|
||||
passwordHistory.push(decryptedPh);
|
||||
}
|
||||
model.passwordHistory = passwordHistory;
|
||||
|
||||
Reference in New Issue
Block a user