mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
Merge remote-tracking branch 'origin' into auth/pm-19877/notification-processing
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CollectionDetailsResponse } from "@bitwarden/admin-console/common";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionAccessSelectionView, CollectionAdminView } from "../models";
|
||||
|
||||
export abstract class CollectionAdminService {
|
||||
getAll: (organizationId: string) => Promise<CollectionAdminView[]>;
|
||||
get: (organizationId: string, collectionId: string) => Promise<CollectionAdminView | undefined>;
|
||||
save: (collection: CollectionAdminView) => Promise<CollectionDetailsResponse>;
|
||||
delete: (organizationId: string, collectionId: string) => Promise<void>;
|
||||
bulkAssignAccess: (
|
||||
abstract getAll(organizationId: string): Promise<CollectionAdminView[]>;
|
||||
abstract get(
|
||||
organizationId: string,
|
||||
collectionId: string,
|
||||
): Promise<CollectionAdminView | undefined>;
|
||||
abstract save(
|
||||
collection: CollectionAdminView,
|
||||
userId: UserId,
|
||||
): Promise<CollectionDetailsResponse>;
|
||||
abstract delete(organizationId: string, collectionId: string): Promise<void>;
|
||||
abstract bulkAssignAccess(
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
users: CollectionAccessSelectionView[],
|
||||
groups: CollectionAccessSelectionView[],
|
||||
) => Promise<void>;
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -9,27 +7,25 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CollectionData, Collection, CollectionView } from "../models";
|
||||
|
||||
export abstract class CollectionService {
|
||||
encryptedCollections$: Observable<Collection[]>;
|
||||
decryptedCollections$: Observable<CollectionView[]>;
|
||||
|
||||
clearActiveUserCache: () => Promise<void>;
|
||||
encrypt: (model: CollectionView) => Promise<Collection>;
|
||||
decryptedCollectionViews$: (ids: CollectionId[]) => Observable<CollectionView[]>;
|
||||
abstract encryptedCollections$(userId: UserId): Observable<Collection[] | null>;
|
||||
abstract decryptedCollections$(userId: UserId): Observable<CollectionView[]>;
|
||||
abstract upsert(collection: CollectionData, userId: UserId): Promise<any>;
|
||||
abstract replace(collections: { [id: string]: CollectionData }, userId: UserId): Promise<any>;
|
||||
/**
|
||||
* @deprecated This method will soon be made private
|
||||
* See PM-12375
|
||||
* @deprecated This method will soon be made private, use `decryptedCollections$` instead.
|
||||
*/
|
||||
decryptMany: (
|
||||
abstract decryptMany$(
|
||||
collections: Collection[],
|
||||
orgKeys?: Record<OrganizationId, OrgKey>,
|
||||
) => Promise<CollectionView[]>;
|
||||
get: (id: string) => Promise<Collection>;
|
||||
getAll: () => Promise<Collection[]>;
|
||||
getAllDecrypted: () => Promise<CollectionView[]>;
|
||||
getAllNested: (collections?: CollectionView[]) => Promise<TreeNode<CollectionView>[]>;
|
||||
getNested: (id: string) => Promise<TreeNode<CollectionView>>;
|
||||
upsert: (collection: CollectionData | CollectionData[]) => Promise<any>;
|
||||
replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise<any>;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
delete: (id: string | string[]) => Promise<any>;
|
||||
orgKeys: Record<OrganizationId, OrgKey>,
|
||||
): Observable<CollectionView[]>;
|
||||
abstract delete(ids: CollectionId[], userId: UserId): Promise<any>;
|
||||
abstract encrypt(model: CollectionView, userId: UserId): Promise<Collection>;
|
||||
/**
|
||||
* Transforms the input CollectionViews into TreeNodes
|
||||
*/
|
||||
abstract getAllNested(collections: CollectionView[]): TreeNode<CollectionView>[];
|
||||
/*
|
||||
* Transforms the input CollectionViews into TreeNodes and then returns the Treenode with the specified id
|
||||
*/
|
||||
abstract getNested(collections: CollectionView[], id: string): TreeNode<CollectionView>;
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { CollectionData, Collection, CollectionView } from "../models";
|
||||
|
||||
export abstract class vNextCollectionService {
|
||||
encryptedCollections$: (userId: UserId) => Observable<Collection[]>;
|
||||
decryptedCollections$: (userId: UserId) => Observable<CollectionView[]>;
|
||||
upsert: (collection: CollectionData | CollectionData[], userId: UserId) => Promise<any>;
|
||||
replace: (collections: { [id: string]: CollectionData }, userId: UserId) => Promise<any>;
|
||||
/**
|
||||
* Clear decrypted state without affecting encrypted state.
|
||||
* Used for locking the vault.
|
||||
*/
|
||||
clearDecryptedState: (userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear decrypted and encrypted state.
|
||||
* Used for logging out.
|
||||
*/
|
||||
clear: (userId: UserId) => Promise<void>;
|
||||
delete: (id: string | string[], userId: UserId) => Promise<any>;
|
||||
encrypt: (model: CollectionView) => Promise<Collection>;
|
||||
/**
|
||||
* @deprecated This method will soon be made private, use `decryptedCollections$` instead.
|
||||
*/
|
||||
decryptMany: (
|
||||
collections: Collection[],
|
||||
orgKeys?: Record<OrganizationId, OrgKey> | null,
|
||||
) => Promise<CollectionView[]>;
|
||||
/**
|
||||
* Transforms the input CollectionViews into TreeNodes
|
||||
*/
|
||||
getAllNested: (collections: CollectionView[]) => TreeNode<CollectionView>[];
|
||||
/**
|
||||
* Transforms the input CollectionViews into TreeNodes and then returns the Treenode with the specified id
|
||||
*/
|
||||
getNested: (collections: CollectionView[], id: string) => TreeNode<CollectionView>;
|
||||
}
|
||||
@@ -26,7 +26,10 @@ export class CollectionData {
|
||||
this.type = response.type;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<CollectionData>) {
|
||||
static fromJSON(obj: Jsonify<CollectionData | null>): CollectionData | null {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return Object.assign(new CollectionData(new CollectionDetailsResponse({})), obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import Domain from "@bitwarden/common/platform/models/domain/domain-base";
|
||||
import Domain, { EncryptableKeys } from "@bitwarden/common/platform/models/domain/domain-base";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { CollectionData } from "./collection.data";
|
||||
@@ -15,16 +13,16 @@ export const CollectionTypes = {
|
||||
export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes];
|
||||
|
||||
export class Collection extends Domain {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: EncString;
|
||||
externalId: string;
|
||||
readOnly: boolean;
|
||||
hidePasswords: boolean;
|
||||
manage: boolean;
|
||||
type: CollectionType;
|
||||
id: string | undefined;
|
||||
organizationId: string | undefined;
|
||||
name: EncString | undefined;
|
||||
externalId: string | undefined;
|
||||
readOnly: boolean = false;
|
||||
hidePasswords: boolean = false;
|
||||
manage: boolean = false;
|
||||
type: CollectionType = CollectionTypes.SharedCollection;
|
||||
|
||||
constructor(obj?: CollectionData) {
|
||||
constructor(obj?: CollectionData | null) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
@@ -51,8 +49,8 @@ export class Collection extends Domain {
|
||||
return this.decryptObj<Collection, CollectionView>(
|
||||
this,
|
||||
new CollectionView(this),
|
||||
["name"],
|
||||
this.organizationId,
|
||||
["name"] as EncryptableKeys<Collection, CollectionView>[],
|
||||
this.organizationId ?? null,
|
||||
orgKey,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export const NestingDelimiter = "/";
|
||||
export class CollectionView implements View, ITreeNodeObject {
|
||||
id: string | undefined;
|
||||
organizationId: string | undefined;
|
||||
name: string | undefined;
|
||||
name: string = "";
|
||||
externalId: string | undefined;
|
||||
// readOnly applies to the items within a collection
|
||||
readOnly: boolean = false;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
COLLECTION_DISK,
|
||||
COLLECTION_MEMORY,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CollectionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { CollectionData, CollectionView } from "../models";
|
||||
|
||||
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<
|
||||
CollectionData | null,
|
||||
CollectionId
|
||||
>(COLLECTION_DISK, "collections", {
|
||||
deserializer: (jsonData: Jsonify<CollectionData | null>) => CollectionData.fromJSON(jsonData),
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
export const DECRYPTED_COLLECTION_DATA_KEY = new UserKeyDefinition<CollectionView[] | null>(
|
||||
COLLECTION_MEMORY,
|
||||
"decryptedCollections",
|
||||
{
|
||||
deserializer: (obj: Jsonify<CollectionView[] | null>) =>
|
||||
obj?.map((f) => CollectionView.fromJSON(f)) ?? null,
|
||||
clearOn: ["logout", "lock"],
|
||||
},
|
||||
);
|
||||
@@ -1,9 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionAdminService, CollectionService } from "../abstractions";
|
||||
@@ -55,7 +57,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
return view;
|
||||
}
|
||||
|
||||
async save(collection: CollectionAdminView): Promise<CollectionDetailsResponse> {
|
||||
async save(collection: CollectionAdminView, userId: UserId): Promise<CollectionDetailsResponse> {
|
||||
const request = await this.encrypt(collection);
|
||||
|
||||
let response: CollectionDetailsResponse;
|
||||
@@ -71,9 +73,9 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
|
||||
}
|
||||
|
||||
if (response.assigned) {
|
||||
await this.collectionService.upsert(new CollectionData(response));
|
||||
await this.collectionService.upsert(new CollectionData(response), userId);
|
||||
} else {
|
||||
await this.collectionService.delete(collection.id);
|
||||
await this.collectionService.delete([collection.id as CollectionId], userId);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { combineLatest, first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
@@ -16,124 +17,382 @@ import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/gu
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionData } from "../models";
|
||||
import { CollectionData, CollectionView } from "../models";
|
||||
|
||||
import {
|
||||
DefaultCollectionService,
|
||||
ENCRYPTED_COLLECTION_DATA_KEY,
|
||||
} from "./default-collection.service";
|
||||
import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state";
|
||||
import { DefaultCollectionService } from "./default-collection.service";
|
||||
|
||||
describe("DefaultCollectionService", () => {
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
let userId: UserId;
|
||||
|
||||
let cryptoKeys: ReplaySubject<Record<OrganizationId, OrgKey> | null>;
|
||||
|
||||
let collectionService: DefaultCollectionService;
|
||||
|
||||
beforeEach(() => {
|
||||
userId = Utils.newGuid() as UserId;
|
||||
|
||||
keyService = mock();
|
||||
encryptService = mock();
|
||||
i18nService = mock();
|
||||
stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
|
||||
cryptoKeys = new ReplaySubject(1);
|
||||
keyService.orgKeys$.mockReturnValue(cryptoKeys);
|
||||
|
||||
// Set up mock decryption
|
||||
encryptService.decryptString
|
||||
.calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey))
|
||||
.mockImplementation((encString, key) =>
|
||||
Promise.resolve(encString.data.replace("ENC_", "DEC_")),
|
||||
);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||
|
||||
// Arrange i18nService so that sorting algorithm doesn't throw
|
||||
i18nService.collator = null;
|
||||
|
||||
collectionService = new DefaultCollectionService(
|
||||
keyService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
stateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as any).bitwardenContainerService;
|
||||
});
|
||||
|
||||
describe("decryptedCollections$", () => {
|
||||
it("emits decrypted collections from state", async () => {
|
||||
// Arrange test collections
|
||||
// Arrange test data
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const org2 = Utils.newGuid() as OrganizationId;
|
||||
|
||||
const orgKey1 = makeSymmetricCryptoKey<OrgKey>(64, 1);
|
||||
const collection1 = collectionDataFactory(org1);
|
||||
|
||||
const org2 = Utils.newGuid() as OrganizationId;
|
||||
const orgKey2 = makeSymmetricCryptoKey<OrgKey>(64, 2);
|
||||
const collection2 = collectionDataFactory(org2);
|
||||
|
||||
// Arrange state provider
|
||||
const fakeStateProvider = mockStateProvider();
|
||||
await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, {
|
||||
[collection1.id]: collection1,
|
||||
[collection2.id]: collection2,
|
||||
// Arrange dependencies
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
cryptoKeys.next({
|
||||
[org1]: orgKey1,
|
||||
[org2]: orgKey2,
|
||||
});
|
||||
|
||||
// Arrange cryptoService - orgKeys and mock decryption
|
||||
const cryptoService = mockCryptoService();
|
||||
cryptoService.orgKeys$.mockReturnValue(
|
||||
of({
|
||||
[org1]: makeSymmetricCryptoKey<OrgKey>(),
|
||||
[org2]: makeSymmetricCryptoKey<OrgKey>(),
|
||||
}),
|
||||
);
|
||||
const result = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
|
||||
const collectionService = new DefaultCollectionService(
|
||||
cryptoService,
|
||||
mock<EncryptService>(),
|
||||
mockI18nService(),
|
||||
fakeStateProvider,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(collectionService.decryptedCollections$);
|
||||
// Assert emitted values
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: collection1.id,
|
||||
name: "DECRYPTED_STRING",
|
||||
});
|
||||
expect(result[1]).toMatchObject({
|
||||
id: collection2.id,
|
||||
name: "DECRYPTED_STRING",
|
||||
});
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: "DEC_NAME_" + collection1.id,
|
||||
},
|
||||
{
|
||||
id: collection2.id,
|
||||
name: "DEC_NAME_" + collection2.id,
|
||||
},
|
||||
]);
|
||||
|
||||
// Assert that the correct org keys were used for each encrypted string
|
||||
// This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection1.name)),
|
||||
orgKey1,
|
||||
);
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection2.name)),
|
||||
orgKey2,
|
||||
);
|
||||
});
|
||||
|
||||
it("emits decrypted collections from in-memory state when available", async () => {
|
||||
// Arrange test data
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const collection1 = collectionViewDataFactory(org1);
|
||||
|
||||
const org2 = Utils.newGuid() as OrganizationId;
|
||||
const collection2 = collectionViewDataFactory(org2);
|
||||
|
||||
await setDecryptedState([collection1, collection2]);
|
||||
|
||||
const result = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
|
||||
// Assert emitted values
|
||||
expect(result.length).toBe(2);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: "DEC_NAME_" + collection1.id,
|
||||
},
|
||||
{
|
||||
id: collection2.id,
|
||||
name: "DEC_NAME_" + collection2.id,
|
||||
},
|
||||
]);
|
||||
|
||||
// Ensure that the returned data came from the in-memory state, rather than from decryption.
|
||||
expect(encryptService.decryptString).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles null collection state", async () => {
|
||||
// Arrange test collections
|
||||
// Arrange dependencies
|
||||
await setEncryptedState(null);
|
||||
cryptoKeys.next({});
|
||||
|
||||
const encryptedCollections = await firstValueFrom(
|
||||
collectionService.encryptedCollections$(userId),
|
||||
);
|
||||
|
||||
expect(encryptedCollections).toBe(null);
|
||||
});
|
||||
|
||||
it("handles undefined orgKeys", (done) => {
|
||||
// Arrange test data
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const collection1 = collectionDataFactory(org1);
|
||||
|
||||
const org2 = Utils.newGuid() as OrganizationId;
|
||||
const collection2 = collectionDataFactory(org2);
|
||||
|
||||
// Arrange state provider
|
||||
const fakeStateProvider = mockStateProvider();
|
||||
await fakeStateProvider.setUserState(ENCRYPTED_COLLECTION_DATA_KEY, null);
|
||||
// Emit a non-null value after the first undefined value has propagated
|
||||
// This will cause the collections to emit, calling done()
|
||||
cryptoKeys.pipe(first()).subscribe((val) => {
|
||||
cryptoKeys.next({});
|
||||
});
|
||||
|
||||
// Arrange cryptoService - orgKeys and mock decryption
|
||||
const cryptoService = mockCryptoService();
|
||||
cryptoService.orgKeys$.mockReturnValue(
|
||||
of({
|
||||
[org1]: makeSymmetricCryptoKey<OrgKey>(),
|
||||
[org2]: makeSymmetricCryptoKey<OrgKey>(),
|
||||
}),
|
||||
);
|
||||
collectionService
|
||||
.decryptedCollections$(userId)
|
||||
.pipe(takeWhile((val) => val.length != 2))
|
||||
.subscribe({ complete: () => done() });
|
||||
|
||||
const collectionService = new DefaultCollectionService(
|
||||
cryptoService,
|
||||
mock<EncryptService>(),
|
||||
mockI18nService(),
|
||||
fakeStateProvider,
|
||||
);
|
||||
// Arrange dependencies
|
||||
void setEncryptedState([collection1, collection2]).then(() => {
|
||||
// Act: emit undefined
|
||||
cryptoKeys.next(undefined);
|
||||
keyService.activeUserOrgKeys$ = of(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
const decryptedCollections = await firstValueFrom(collectionService.decryptedCollections$);
|
||||
expect(decryptedCollections.length).toBe(0);
|
||||
it("Decrypts one time for multiple simultaneous callers", async () => {
|
||||
const decryptedMock: CollectionView[] = [{ id: "col1" }] as CollectionView[];
|
||||
const decryptManySpy = jest
|
||||
.spyOn(collectionService, "decryptMany$")
|
||||
.mockReturnValue(of(decryptedMock));
|
||||
|
||||
const encryptedCollections = await firstValueFrom(collectionService.encryptedCollections$);
|
||||
expect(encryptedCollections.length).toBe(0);
|
||||
jest
|
||||
.spyOn(collectionService as any, "encryptedCollections$")
|
||||
.mockReturnValue(of([{ id: "enc1" }]));
|
||||
jest.spyOn(keyService, "orgKeys$").mockReturnValue(of({ key: "fake-key" }));
|
||||
|
||||
// Simulate multiple subscribers
|
||||
const obs1 = collectionService.decryptedCollections$(userId);
|
||||
const obs2 = collectionService.decryptedCollections$(userId);
|
||||
const obs3 = collectionService.decryptedCollections$(userId);
|
||||
|
||||
await firstValueFrom(combineLatest([obs1, obs2, obs3]));
|
||||
|
||||
// Expect decryptMany$ to be called only once
|
||||
expect(decryptManySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptedCollections$", () => {
|
||||
it("emits encrypted collections from state", async () => {
|
||||
// Arrange test data
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
|
||||
// Arrange dependencies
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
|
||||
expect(result!.length).toBe(2);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: makeEncString("ENC_NAME_" + collection1.id),
|
||||
},
|
||||
{
|
||||
id: collection2.id,
|
||||
name: makeEncString("ENC_NAME_" + collection2.id),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles null collection state", async () => {
|
||||
await setEncryptedState(null);
|
||||
|
||||
const decryptedCollections = await firstValueFrom(
|
||||
collectionService.encryptedCollections$(userId),
|
||||
);
|
||||
expect(decryptedCollections).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsert", () => {
|
||||
it("upserts to existing collections", async () => {
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const orgKey1 = makeSymmetricCryptoKey<OrgKey>(64, 1);
|
||||
const collection1 = collectionDataFactory(org1);
|
||||
|
||||
await setEncryptedState([collection1]);
|
||||
cryptoKeys.next({
|
||||
[collection1.organizationId]: orgKey1,
|
||||
});
|
||||
|
||||
const updatedCollection1 = Object.assign(new CollectionData({} as any), collection1, {
|
||||
name: makeEncString("UPDATED_ENC_NAME_" + collection1.id).encryptedString,
|
||||
});
|
||||
|
||||
await collectionService.upsert(updatedCollection1, userId);
|
||||
|
||||
const encryptedResult = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
|
||||
expect(encryptedResult!.length).toBe(1);
|
||||
expect(encryptedResult).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: makeEncString("UPDATED_ENC_NAME_" + collection1.id),
|
||||
},
|
||||
]);
|
||||
|
||||
const decryptedResult = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
expect(decryptedResult.length).toBe(1);
|
||||
expect(decryptedResult).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: "UPDATED_DEC_NAME_" + collection1.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("upserts to a null state", async () => {
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const orgKey1 = makeSymmetricCryptoKey<OrgKey>(64, 1);
|
||||
const collection1 = collectionDataFactory(org1);
|
||||
|
||||
cryptoKeys.next({
|
||||
[collection1.organizationId]: orgKey1,
|
||||
});
|
||||
|
||||
await setEncryptedState(null);
|
||||
|
||||
await collectionService.upsert(collection1, userId);
|
||||
|
||||
const encryptedResult = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(encryptedResult!.length).toBe(1);
|
||||
expect(encryptedResult).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: makeEncString("ENC_NAME_" + collection1.id),
|
||||
},
|
||||
]);
|
||||
|
||||
const decryptedResult = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
expect(decryptedResult.length).toBe(1);
|
||||
expect(decryptedResult).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: "DEC_NAME_" + collection1.id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replace", () => {
|
||||
it("replaces all collections", async () => {
|
||||
await setEncryptedState([collectionDataFactory(), collectionDataFactory()]);
|
||||
|
||||
const newCollection3 = collectionDataFactory();
|
||||
await collectionService.replace(
|
||||
{
|
||||
[newCollection3.id]: newCollection3,
|
||||
},
|
||||
userId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result!.length).toBe(1);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: newCollection3.id,
|
||||
name: makeEncString("ENC_NAME_" + newCollection3.id),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
it("deletes a collection", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
|
||||
await collectionService.delete([collection1.id], userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result!.length).toEqual(1);
|
||||
expect(result![0]).toMatchObject({ id: collection2.id });
|
||||
});
|
||||
|
||||
it("deletes several collections", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
const collection3 = collectionDataFactory();
|
||||
await setEncryptedState([collection1, collection2, collection3]);
|
||||
|
||||
await collectionService.delete([collection1.id, collection3.id], userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result!.length).toEqual(1);
|
||||
expect(result![0]).toMatchObject({ id: collection2.id });
|
||||
});
|
||||
|
||||
it("handles null collections", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
await setEncryptedState(null);
|
||||
|
||||
await collectionService.delete([collection1.id], userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result!.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
const setEncryptedState = (collectionData: CollectionData[] | null) =>
|
||||
stateProvider.setUserState(
|
||||
ENCRYPTED_COLLECTION_DATA_KEY,
|
||||
collectionData == null ? null : Object.fromEntries(collectionData.map((c) => [c.id, c])),
|
||||
userId,
|
||||
);
|
||||
|
||||
const setDecryptedState = (collectionViews: CollectionView[] | null) =>
|
||||
stateProvider.setUserState(DECRYPTED_COLLECTION_DATA_KEY, collectionViews, userId);
|
||||
});
|
||||
|
||||
const mockI18nService = () => {
|
||||
const i18nService = mock<I18nService>();
|
||||
i18nService.collator = null; // this is a mock only, avoid use of this object
|
||||
return i18nService;
|
||||
};
|
||||
|
||||
const mockStateProvider = () => {
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
return new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
};
|
||||
|
||||
const mockCryptoService = () => {
|
||||
const keyService = mock<KeyService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
encryptService.decryptString
|
||||
.calledWith(expect.any(EncString), expect.anything())
|
||||
.mockResolvedValue("DECRYPTED_STRING");
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||
|
||||
return keyService;
|
||||
};
|
||||
|
||||
const collectionDataFactory = (orgId: OrganizationId) => {
|
||||
const collectionDataFactory = (orgId?: OrganizationId) => {
|
||||
const collection = new CollectionData({} as any);
|
||||
collection.id = Utils.newGuid() as CollectionId;
|
||||
collection.organizationId = orgId;
|
||||
collection.name = makeEncString("ENC_STRING").encryptedString;
|
||||
collection.organizationId = orgId ?? (Utils.newGuid() as OrganizationId);
|
||||
collection.name = makeEncString("ENC_NAME_" + collection.id).encryptedString ?? "";
|
||||
|
||||
return collection;
|
||||
};
|
||||
|
||||
function collectionViewDataFactory(orgId?: OrganizationId): CollectionView {
|
||||
const collectionView = new CollectionView();
|
||||
collectionView.id = Utils.newGuid() as CollectionId;
|
||||
collectionView.organizationId = orgId ?? (Utils.newGuid() as OrganizationId);
|
||||
collectionView.name = "DEC_NAME_" + collectionView.id;
|
||||
return collectionView;
|
||||
}
|
||||
|
||||
@@ -1,113 +1,193 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
import {
|
||||
combineLatest,
|
||||
delayWhen,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
NEVER,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
ActiveUserState,
|
||||
COLLECTION_DATA,
|
||||
DeriveDefinition,
|
||||
DerivedState,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionService } from "../abstractions";
|
||||
import { CollectionService } from "../abstractions/collection.service";
|
||||
import { Collection, CollectionData, CollectionView } from "../models";
|
||||
|
||||
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
|
||||
COLLECTION_DATA,
|
||||
"collections",
|
||||
{
|
||||
deserializer: (jsonData: Jsonify<CollectionData>) => CollectionData.fromJSON(jsonData),
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
const DECRYPTED_COLLECTION_DATA_KEY = new DeriveDefinition<
|
||||
[Record<CollectionId, CollectionData>, Record<OrganizationId, OrgKey>],
|
||||
CollectionView[],
|
||||
{ collectionService: DefaultCollectionService }
|
||||
>(COLLECTION_DATA, "decryptedCollections", {
|
||||
deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)),
|
||||
derive: async ([collections, orgKeys], { collectionService }) => {
|
||||
if (collections == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = Object.values(collections).map((c) => new Collection(c));
|
||||
return await collectionService.decryptMany(data, orgKeys);
|
||||
},
|
||||
});
|
||||
import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
export class DefaultCollectionService implements CollectionService {
|
||||
private encryptedCollectionDataState: ActiveUserState<Record<CollectionId, CollectionData>>;
|
||||
encryptedCollections$: Observable<Collection[]>;
|
||||
private decryptedCollectionDataState: DerivedState<CollectionView[]>;
|
||||
decryptedCollections$: Observable<CollectionView[]>;
|
||||
|
||||
decryptedCollectionViews$(ids: CollectionId[]): Observable<CollectionView[]> {
|
||||
return this.decryptedCollections$.pipe(
|
||||
map((collections) => collections.filter((c) => ids.includes(c.id as CollectionId))),
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
private i18nService: I18nService,
|
||||
protected stateProvider: StateProvider,
|
||||
) {
|
||||
this.encryptedCollectionDataState = this.stateProvider.getActive(ENCRYPTED_COLLECTION_DATA_KEY);
|
||||
) {}
|
||||
|
||||
this.encryptedCollections$ = this.encryptedCollectionDataState.state$.pipe(
|
||||
private collectionViewCache = new Map<UserId, Observable<CollectionView[]>>();
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for encrypted collection data.
|
||||
*/
|
||||
private encryptedState(
|
||||
userId: UserId,
|
||||
): SingleUserState<Record<CollectionId, CollectionData | null>> {
|
||||
return this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for decrypted collection data.
|
||||
*/
|
||||
private decryptedState(userId: UserId): SingleUserState<CollectionView[] | null> {
|
||||
return this.stateProvider.getUser(userId, DECRYPTED_COLLECTION_DATA_KEY);
|
||||
}
|
||||
|
||||
encryptedCollections$(userId: UserId): Observable<Collection[] | null> {
|
||||
return this.encryptedState(userId).state$.pipe(
|
||||
map((collections) => {
|
||||
if (collections == null) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.values(collections).map((c) => new Collection(c));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const encryptedCollectionsWithKeys = this.encryptedCollectionDataState.combinedState$.pipe(
|
||||
switchMap(([userId, collectionData]) =>
|
||||
combineLatest([of(collectionData), this.keyService.orgKeys$(userId)]),
|
||||
decryptedCollections$(userId: UserId): Observable<CollectionView[]> {
|
||||
const cachedResult = this.collectionViewCache.get(userId);
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const result$ = this.decryptedState(userId).state$.pipe(
|
||||
switchMap((decryptedState) => {
|
||||
// If decrypted state is already populated, return that
|
||||
if (decryptedState !== null) {
|
||||
return of(decryptedState ?? []);
|
||||
}
|
||||
|
||||
return this.initializeDecryptedState(userId).pipe(switchMap(() => NEVER));
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
this.collectionViewCache.set(userId, result$);
|
||||
return result$;
|
||||
}
|
||||
|
||||
private initializeDecryptedState(userId: UserId): Observable<CollectionView[]> {
|
||||
return combineLatest([
|
||||
this.encryptedCollections$(userId),
|
||||
this.keyService.orgKeys$(userId).pipe(filter((orgKeys) => !!orgKeys)),
|
||||
]).pipe(
|
||||
switchMap(([collections, orgKeys]) =>
|
||||
this.decryptMany$(collections, orgKeys).pipe(
|
||||
delayWhen((collections) => this.setDecryptedCollections(collections, userId)),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.decryptedCollectionDataState = this.stateProvider.getDerived(
|
||||
encryptedCollectionsWithKeys,
|
||||
DECRYPTED_COLLECTION_DATA_KEY,
|
||||
{ collectionService: this },
|
||||
);
|
||||
|
||||
this.decryptedCollections$ = this.decryptedCollectionDataState.state$;
|
||||
}
|
||||
|
||||
async clearActiveUserCache(): Promise<void> {
|
||||
await this.decryptedCollectionDataState.forceValue(null);
|
||||
async upsert(toUpdate: CollectionData, userId: UserId): Promise<void> {
|
||||
if (toUpdate == null) {
|
||||
return;
|
||||
}
|
||||
await this.encryptedState(userId).update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
collections[toUpdate.id] = toUpdate;
|
||||
|
||||
return collections;
|
||||
});
|
||||
|
||||
const decryptedCollections = await firstValueFrom(
|
||||
this.keyService.orgKeys$(userId).pipe(
|
||||
switchMap((orgKeys) => {
|
||||
if (!orgKeys) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
return this.decryptMany$([new Collection(toUpdate)], orgKeys);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await this.decryptedState(userId).update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = [];
|
||||
}
|
||||
|
||||
if (!decryptedCollections?.length) {
|
||||
return collections;
|
||||
}
|
||||
|
||||
const decryptedCollection = decryptedCollections[0];
|
||||
const existingIndex = collections.findIndex((collection) => collection.id == toUpdate.id);
|
||||
if (existingIndex >= 0) {
|
||||
collections[existingIndex] = decryptedCollection;
|
||||
} else {
|
||||
collections.push(decryptedCollection);
|
||||
}
|
||||
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
async encrypt(model: CollectionView): Promise<Collection> {
|
||||
async replace(collections: Record<CollectionId, CollectionData>, userId: UserId): Promise<void> {
|
||||
await this.encryptedState(userId).update(() => collections);
|
||||
await this.decryptedState(userId).update(() => null);
|
||||
}
|
||||
|
||||
async delete(ids: CollectionId[], userId: UserId): Promise<any> {
|
||||
await this.encryptedState(userId).update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
ids.forEach((i) => {
|
||||
delete collections[i];
|
||||
});
|
||||
return collections;
|
||||
});
|
||||
|
||||
await this.decryptedState(userId).update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = [];
|
||||
}
|
||||
ids.forEach((i) => {
|
||||
if (collections?.length) {
|
||||
collections = collections.filter((c) => c.id != i) ?? [];
|
||||
}
|
||||
});
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
async encrypt(model: CollectionView, userId: UserId): Promise<Collection> {
|
||||
if (model.organizationId == null) {
|
||||
throw new Error("Collection has no organization id.");
|
||||
}
|
||||
const key = await this.keyService.getOrgKey(model.organizationId);
|
||||
if (key == null) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
|
||||
const key = await firstValueFrom(
|
||||
this.keyService.orgKeys$(userId).pipe(
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((k) => k[model.organizationId as OrganizationId]),
|
||||
),
|
||||
);
|
||||
|
||||
const collection = new Collection();
|
||||
collection.id = model.id;
|
||||
collection.organizationId = model.organizationId;
|
||||
@@ -117,58 +197,37 @@ export class DefaultCollectionService implements CollectionService {
|
||||
return collection;
|
||||
}
|
||||
|
||||
// TODO: this should be private and orgKeys should be required.
|
||||
// TODO: this should be private.
|
||||
// See https://bitwarden.atlassian.net/browse/PM-12375
|
||||
async decryptMany(
|
||||
collections: Collection[],
|
||||
orgKeys?: Record<OrganizationId, OrgKey>,
|
||||
): Promise<CollectionView[]> {
|
||||
if (collections == null || collections.length === 0) {
|
||||
return [];
|
||||
decryptMany$(
|
||||
collections: Collection[] | null,
|
||||
orgKeys: Record<OrganizationId, OrgKey>,
|
||||
): Observable<CollectionView[]> {
|
||||
if (collections === null || collections.length == 0 || orgKeys === null) {
|
||||
return of([]);
|
||||
}
|
||||
const decCollections: CollectionView[] = [];
|
||||
|
||||
orgKeys ??= await firstValueFrom(this.keyService.activeUserOrgKeys$);
|
||||
const decCollections: Observable<CollectionView>[] = [];
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
collections.forEach((collection) => {
|
||||
promises.push(
|
||||
collection
|
||||
.decrypt(orgKeys[collection.organizationId as OrganizationId])
|
||||
.then((c) => decCollections.push(c)),
|
||||
decCollections.push(
|
||||
from(collection.decrypt(orgKeys[collection.organizationId as OrganizationId])),
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
return decCollections.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Collection> {
|
||||
return (
|
||||
(await firstValueFrom(
|
||||
this.encryptedCollections$.pipe(map((cs) => cs.find((c) => c.id === id))),
|
||||
)) ?? null
|
||||
return combineLatest(decCollections).pipe(
|
||||
map((collections) => collections.sort(Utils.getSortFunction(this.i18nService, "name"))),
|
||||
);
|
||||
}
|
||||
|
||||
async getAll(): Promise<Collection[]> {
|
||||
return await firstValueFrom(this.encryptedCollections$);
|
||||
}
|
||||
|
||||
async getAllDecrypted(): Promise<CollectionView[]> {
|
||||
return await firstValueFrom(this.decryptedCollections$);
|
||||
}
|
||||
|
||||
async getAllNested(collections: CollectionView[] = null): Promise<TreeNode<CollectionView>[]> {
|
||||
if (collections == null) {
|
||||
collections = await this.getAllDecrypted();
|
||||
}
|
||||
getAllNested(collections: CollectionView[]): TreeNode<CollectionView>[] {
|
||||
const nodes: TreeNode<CollectionView>[] = [];
|
||||
collections.forEach((c) => {
|
||||
const collectionCopy = new CollectionView();
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
@@ -177,58 +236,23 @@ export class DefaultCollectionService implements CollectionService {
|
||||
* @deprecated August 30 2022: Moved to new Vault Filter Service
|
||||
* Remove when Desktop and Browser are updated
|
||||
*/
|
||||
async getNested(id: string): Promise<TreeNode<CollectionView>> {
|
||||
const collections = await this.getAllNested();
|
||||
return ServiceUtils.getTreeNodeObjectFromList(collections, id) as TreeNode<CollectionView>;
|
||||
getNested(collections: CollectionView[], id: string): TreeNode<CollectionView> {
|
||||
const nestedCollections = this.getAllNested(collections);
|
||||
return ServiceUtils.getTreeNodeObjectFromList(
|
||||
nestedCollections,
|
||||
id,
|
||||
) as TreeNode<CollectionView>;
|
||||
}
|
||||
|
||||
async upsert(toUpdate: CollectionData | CollectionData[]): Promise<void> {
|
||||
if (toUpdate == null) {
|
||||
return;
|
||||
}
|
||||
await this.encryptedCollectionDataState.update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
if (Array.isArray(toUpdate)) {
|
||||
toUpdate.forEach((c) => {
|
||||
collections[c.id] = c;
|
||||
});
|
||||
} else {
|
||||
collections[toUpdate.id] = toUpdate;
|
||||
}
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(collections: Record<CollectionId, CollectionData>, userId: UserId): Promise<void> {
|
||||
await this.stateProvider
|
||||
.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY)
|
||||
.update(() => collections);
|
||||
}
|
||||
|
||||
async clear(userId?: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
await this.encryptedCollectionDataState.update(() => null);
|
||||
await this.decryptedCollectionDataState.forceValue(null);
|
||||
} else {
|
||||
await this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY).update(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: CollectionId | CollectionId[]): Promise<any> {
|
||||
await this.encryptedCollectionDataState.update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
if (typeof id === "string") {
|
||||
delete collections[id];
|
||||
} else {
|
||||
(id as CollectionId[]).forEach((i) => {
|
||||
delete collections[i];
|
||||
});
|
||||
}
|
||||
return collections;
|
||||
});
|
||||
/**
|
||||
* Sets the decrypted collections state for a user.
|
||||
* @param collections the decrypted collections
|
||||
* @param userId the user id
|
||||
*/
|
||||
private async setDecryptedCollections(
|
||||
collections: CollectionView[],
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
await this.stateProvider.setUserState(DECRYPTED_COLLECTION_DATA_KEY, collections, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { first, firstValueFrom, of, ReplaySubject, takeWhile } from "rxjs";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import {
|
||||
FakeStateProvider,
|
||||
makeEncString,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CollectionData } from "../models";
|
||||
|
||||
import { DefaultvNextCollectionService } from "./default-vnext-collection.service";
|
||||
import { ENCRYPTED_COLLECTION_DATA_KEY } from "./vnext-collection.state";
|
||||
|
||||
describe("DefaultvNextCollectionService", () => {
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let stateProvider: FakeStateProvider;
|
||||
|
||||
let userId: UserId;
|
||||
|
||||
let cryptoKeys: ReplaySubject<Record<OrganizationId, OrgKey> | null>;
|
||||
|
||||
let collectionService: DefaultvNextCollectionService;
|
||||
|
||||
beforeEach(() => {
|
||||
userId = Utils.newGuid() as UserId;
|
||||
|
||||
keyService = mock();
|
||||
encryptService = mock();
|
||||
i18nService = mock();
|
||||
stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
|
||||
cryptoKeys = new ReplaySubject(1);
|
||||
keyService.orgKeys$.mockReturnValue(cryptoKeys);
|
||||
|
||||
// Set up mock decryption
|
||||
encryptService.decryptString
|
||||
.calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey))
|
||||
.mockImplementation((encString, key) =>
|
||||
Promise.resolve(encString.data.replace("ENC_", "DEC_")),
|
||||
);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||
|
||||
// Arrange i18nService so that sorting algorithm doesn't throw
|
||||
i18nService.collator = null;
|
||||
|
||||
collectionService = new DefaultvNextCollectionService(
|
||||
keyService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
stateProvider,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as any).bitwardenContainerService;
|
||||
});
|
||||
|
||||
describe("decryptedCollections$", () => {
|
||||
it("emits decrypted collections from state", async () => {
|
||||
// Arrange test data
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const orgKey1 = makeSymmetricCryptoKey<OrgKey>(64, 1);
|
||||
const collection1 = collectionDataFactory(org1);
|
||||
|
||||
const org2 = Utils.newGuid() as OrganizationId;
|
||||
const orgKey2 = makeSymmetricCryptoKey<OrgKey>(64, 2);
|
||||
const collection2 = collectionDataFactory(org2);
|
||||
|
||||
// Arrange dependencies
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
cryptoKeys.next({
|
||||
[org1]: orgKey1,
|
||||
[org2]: orgKey2,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
|
||||
// Assert emitted values
|
||||
expect(result.length).toBe(2);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: "DEC_NAME_" + collection1.id,
|
||||
},
|
||||
{
|
||||
id: collection2.id,
|
||||
name: "DEC_NAME_" + collection2.id,
|
||||
},
|
||||
]);
|
||||
|
||||
// Assert that the correct org keys were used for each encrypted string
|
||||
// This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection1.name)),
|
||||
orgKey1,
|
||||
);
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection2.name)),
|
||||
orgKey2,
|
||||
);
|
||||
});
|
||||
|
||||
it("handles null collection state", async () => {
|
||||
// Arrange dependencies
|
||||
await setEncryptedState(null);
|
||||
cryptoKeys.next({});
|
||||
|
||||
const encryptedCollections = await firstValueFrom(
|
||||
collectionService.encryptedCollections$(userId),
|
||||
);
|
||||
|
||||
expect(encryptedCollections.length).toBe(0);
|
||||
});
|
||||
|
||||
it("handles undefined orgKeys", (done) => {
|
||||
// Arrange test data
|
||||
const org1 = Utils.newGuid() as OrganizationId;
|
||||
const collection1 = collectionDataFactory(org1);
|
||||
|
||||
const org2 = Utils.newGuid() as OrganizationId;
|
||||
const collection2 = collectionDataFactory(org2);
|
||||
|
||||
// Emit a non-null value after the first undefined value has propagated
|
||||
// This will cause the collections to emit, calling done()
|
||||
cryptoKeys.pipe(first()).subscribe((val) => {
|
||||
cryptoKeys.next({});
|
||||
});
|
||||
|
||||
collectionService
|
||||
.decryptedCollections$(userId)
|
||||
.pipe(takeWhile((val) => val.length != 2))
|
||||
.subscribe({ complete: () => done() });
|
||||
|
||||
// Arrange dependencies
|
||||
void setEncryptedState([collection1, collection2]).then(() => {
|
||||
// Act: emit undefined
|
||||
cryptoKeys.next(undefined);
|
||||
keyService.activeUserOrgKeys$ = of(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptedCollections$", () => {
|
||||
it("emits encrypted collections from state", async () => {
|
||||
// Arrange test data
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
|
||||
// Arrange dependencies
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: makeEncString("ENC_NAME_" + collection1.id),
|
||||
},
|
||||
{
|
||||
id: collection2.id,
|
||||
name: makeEncString("ENC_NAME_" + collection2.id),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles null collection state", async () => {
|
||||
await setEncryptedState(null);
|
||||
|
||||
const decryptedCollections = await firstValueFrom(
|
||||
collectionService.encryptedCollections$(userId),
|
||||
);
|
||||
expect(decryptedCollections.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsert", () => {
|
||||
it("upserts to existing collections", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
|
||||
const updatedCollection1 = Object.assign(new CollectionData({} as any), collection1, {
|
||||
name: makeEncString("UPDATED_ENC_NAME_" + collection1.id).encryptedString,
|
||||
});
|
||||
const newCollection3 = collectionDataFactory();
|
||||
|
||||
await collectionService.upsert([updatedCollection1, newCollection3], userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result.length).toBe(3);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: makeEncString("UPDATED_ENC_NAME_" + collection1.id),
|
||||
},
|
||||
{
|
||||
id: collection2.id,
|
||||
name: makeEncString("ENC_NAME_" + collection2.id),
|
||||
},
|
||||
{
|
||||
id: newCollection3.id,
|
||||
name: makeEncString("ENC_NAME_" + newCollection3.id),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("upserts to a null state", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
|
||||
await setEncryptedState(null);
|
||||
|
||||
await collectionService.upsert(collection1, userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result.length).toBe(1);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: collection1.id,
|
||||
name: makeEncString("ENC_NAME_" + collection1.id),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replace", () => {
|
||||
it("replaces all collections", async () => {
|
||||
await setEncryptedState([collectionDataFactory(), collectionDataFactory()]);
|
||||
|
||||
const newCollection3 = collectionDataFactory();
|
||||
await collectionService.replace(
|
||||
{
|
||||
[newCollection3.id]: newCollection3,
|
||||
},
|
||||
userId,
|
||||
);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result.length).toBe(1);
|
||||
expect(result).toContainPartialObjects([
|
||||
{
|
||||
id: newCollection3.id,
|
||||
name: makeEncString("ENC_NAME_" + newCollection3.id),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("clearDecryptedState", async () => {
|
||||
await setEncryptedState([collectionDataFactory(), collectionDataFactory()]);
|
||||
|
||||
await collectionService.clearDecryptedState(userId);
|
||||
|
||||
// Encrypted state remains
|
||||
const encryptedState = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(encryptedState.length).toEqual(2);
|
||||
|
||||
// Decrypted state is cleared
|
||||
const decryptedState = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
expect(decryptedState.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("clear", async () => {
|
||||
await setEncryptedState([collectionDataFactory(), collectionDataFactory()]);
|
||||
cryptoKeys.next({});
|
||||
|
||||
await collectionService.clear(userId);
|
||||
|
||||
// Encrypted state is cleared
|
||||
const encryptedState = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(encryptedState.length).toEqual(0);
|
||||
|
||||
// Decrypted state is cleared
|
||||
const decryptedState = await firstValueFrom(collectionService.decryptedCollections$(userId));
|
||||
expect(decryptedState.length).toEqual(0);
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
it("deletes a collection", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
await setEncryptedState([collection1, collection2]);
|
||||
|
||||
await collectionService.delete(collection1.id, userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toMatchObject({ id: collection2.id });
|
||||
});
|
||||
|
||||
it("deletes several collections", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
const collection2 = collectionDataFactory();
|
||||
const collection3 = collectionDataFactory();
|
||||
await setEncryptedState([collection1, collection2, collection3]);
|
||||
|
||||
await collectionService.delete([collection1.id, collection3.id], userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toMatchObject({ id: collection2.id });
|
||||
});
|
||||
|
||||
it("handles null collections", async () => {
|
||||
const collection1 = collectionDataFactory();
|
||||
await setEncryptedState(null);
|
||||
|
||||
await collectionService.delete(collection1.id, userId);
|
||||
|
||||
const result = await firstValueFrom(collectionService.encryptedCollections$(userId));
|
||||
expect(result.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
const setEncryptedState = (collectionData: CollectionData[] | null) =>
|
||||
stateProvider.setUserState(
|
||||
ENCRYPTED_COLLECTION_DATA_KEY,
|
||||
collectionData == null ? null : Object.fromEntries(collectionData.map((c) => [c.id, c])),
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
const collectionDataFactory = (orgId?: OrganizationId) => {
|
||||
const collection = new CollectionData({} as any);
|
||||
collection.id = Utils.newGuid() as CollectionId;
|
||||
collection.organizationId = orgId ?? (Utils.newGuid() as OrganizationId);
|
||||
collection.name = makeEncString("ENC_NAME_" + collection.id).encryptedString;
|
||||
|
||||
return collection;
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { combineLatest, filter, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider, DerivedState } from "@bitwarden/common/platform/state";
|
||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { vNextCollectionService } from "../abstractions/vnext-collection.service";
|
||||
import { Collection, CollectionData, CollectionView } from "../models";
|
||||
|
||||
import {
|
||||
DECRYPTED_COLLECTION_DATA_KEY,
|
||||
ENCRYPTED_COLLECTION_DATA_KEY,
|
||||
} from "./vnext-collection.state";
|
||||
|
||||
const NestingDelimiter = "/";
|
||||
|
||||
export class DefaultvNextCollectionService implements vNextCollectionService {
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
private i18nService: I18nService,
|
||||
protected stateProvider: StateProvider,
|
||||
) {}
|
||||
|
||||
encryptedCollections$(userId: UserId) {
|
||||
return this.encryptedState(userId).state$.pipe(
|
||||
map((collections) => {
|
||||
if (collections == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.values(collections).map((c) => new Collection(c));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
decryptedCollections$(userId: UserId) {
|
||||
return this.decryptedState(userId).state$.pipe(map((collections) => collections ?? []));
|
||||
}
|
||||
|
||||
async upsert(toUpdate: CollectionData | CollectionData[], userId: UserId): Promise<void> {
|
||||
if (toUpdate == null) {
|
||||
return;
|
||||
}
|
||||
await this.encryptedState(userId).update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
if (Array.isArray(toUpdate)) {
|
||||
toUpdate.forEach((c) => {
|
||||
collections[c.id] = c;
|
||||
});
|
||||
} else {
|
||||
collections[toUpdate.id] = toUpdate;
|
||||
}
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(collections: Record<CollectionId, CollectionData>, userId: UserId): Promise<void> {
|
||||
await this.encryptedState(userId).update(() => collections);
|
||||
}
|
||||
|
||||
async clearDecryptedState(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
|
||||
await this.decryptedState(userId).forceValue([]);
|
||||
}
|
||||
|
||||
async clear(userId: UserId): Promise<void> {
|
||||
await this.encryptedState(userId).update(() => null);
|
||||
// This will propagate from the encrypted state update, but by doing it explicitly
|
||||
// the promise doesn't resolve until the update is complete.
|
||||
await this.decryptedState(userId).forceValue([]);
|
||||
}
|
||||
|
||||
async delete(id: CollectionId | CollectionId[], userId: UserId): Promise<any> {
|
||||
await this.encryptedState(userId).update((collections) => {
|
||||
if (collections == null) {
|
||||
collections = {};
|
||||
}
|
||||
if (typeof id === "string") {
|
||||
delete collections[id];
|
||||
} else {
|
||||
(id as CollectionId[]).forEach((i) => {
|
||||
delete collections[i];
|
||||
});
|
||||
}
|
||||
return collections;
|
||||
});
|
||||
}
|
||||
|
||||
async encrypt(model: CollectionView): Promise<Collection> {
|
||||
if (model.organizationId == null) {
|
||||
throw new Error("Collection has no organization id.");
|
||||
}
|
||||
const key = await this.keyService.getOrgKey(model.organizationId);
|
||||
if (key == null) {
|
||||
throw new Error("No key for this collection's organization.");
|
||||
}
|
||||
const collection = new Collection();
|
||||
collection.id = model.id;
|
||||
collection.organizationId = model.organizationId;
|
||||
collection.readOnly = model.readOnly;
|
||||
collection.externalId = model.externalId;
|
||||
collection.name = await this.encryptService.encryptString(model.name, key);
|
||||
return collection;
|
||||
}
|
||||
|
||||
// TODO: this should be private and orgKeys should be required.
|
||||
// See https://bitwarden.atlassian.net/browse/PM-12375
|
||||
async decryptMany(
|
||||
collections: Collection[],
|
||||
orgKeys?: Record<OrganizationId, OrgKey> | null,
|
||||
): Promise<CollectionView[]> {
|
||||
if (collections == null || collections.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const decCollections: CollectionView[] = [];
|
||||
|
||||
orgKeys ??= await firstValueFrom(this.keyService.activeUserOrgKeys$);
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
collections.forEach((collection) => {
|
||||
promises.push(
|
||||
collection
|
||||
.decrypt(orgKeys[collection.organizationId as OrganizationId])
|
||||
.then((c) => decCollections.push(c)),
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
return decCollections.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
}
|
||||
|
||||
getAllNested(collections: CollectionView[]): TreeNode<CollectionView>[] {
|
||||
const nodes: TreeNode<CollectionView>[] = [];
|
||||
collections.forEach((c) => {
|
||||
const collectionCopy = new CollectionView();
|
||||
collectionCopy.id = c.id;
|
||||
collectionCopy.organizationId = c.organizationId;
|
||||
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
|
||||
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated August 30 2022: Moved to new Vault Filter Service
|
||||
* Remove when Desktop and Browser are updated
|
||||
*/
|
||||
getNested(collections: CollectionView[], id: string): TreeNode<CollectionView> {
|
||||
const nestedCollections = this.getAllNested(collections);
|
||||
return ServiceUtils.getTreeNodeObjectFromList(
|
||||
nestedCollections,
|
||||
id,
|
||||
) as TreeNode<CollectionView>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for encrypted collection data.
|
||||
*/
|
||||
private encryptedState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a SingleUserState for decrypted collection data.
|
||||
*/
|
||||
private decryptedState(userId: UserId): DerivedState<CollectionView[]> {
|
||||
const encryptedCollectionsWithKeys$ = combineLatest([
|
||||
this.encryptedCollections$(userId),
|
||||
// orgKeys$ can emit null during brief moments on unlock and lock/logout, we want to ignore those intermediate states
|
||||
this.keyService.orgKeys$(userId).pipe(filter((orgKeys) => orgKeys != null)),
|
||||
]);
|
||||
|
||||
return this.stateProvider.getDerived(
|
||||
encryptedCollectionsWithKeys$,
|
||||
DECRYPTED_COLLECTION_DATA_KEY,
|
||||
{
|
||||
collectionService: this,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
COLLECTION_DATA,
|
||||
DeriveDefinition,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import { vNextCollectionService } from "../abstractions/vnext-collection.service";
|
||||
import { Collection, CollectionData, CollectionView } from "../models";
|
||||
|
||||
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
|
||||
COLLECTION_DATA,
|
||||
"collections",
|
||||
{
|
||||
deserializer: (jsonData: Jsonify<CollectionData>) => CollectionData.fromJSON(jsonData),
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export const DECRYPTED_COLLECTION_DATA_KEY = new DeriveDefinition<
|
||||
[Collection[], Record<OrganizationId, OrgKey> | null],
|
||||
CollectionView[],
|
||||
{ collectionService: vNextCollectionService }
|
||||
>(COLLECTION_DATA, "decryptedCollections", {
|
||||
deserializer: (obj) => obj.map((collection) => CollectionView.fromJSON(collection)),
|
||||
derive: async ([collections, orgKeys], { collectionService }) => {
|
||||
if (collections == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await collectionService.decryptMany(collections, orgKeys);
|
||||
},
|
||||
});
|
||||
@@ -1,232 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
|
||||
|
||||
@Directive()
|
||||
export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
masterPassword: string;
|
||||
masterPasswordRetype: string;
|
||||
formPromise: Promise<any>;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
passwordStrengthResult: any;
|
||||
color: string;
|
||||
text: string;
|
||||
leakedPassword: boolean;
|
||||
minimumLength = Utils.minimumPasswordLength;
|
||||
|
||||
protected email: string;
|
||||
protected kdfConfig: KdfConfig;
|
||||
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
protected dialogService: DialogService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected messagingService: MessagingService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyService: PolicyService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
if (this.enforcedPolicyOptions?.minLength) {
|
||||
this.minimumLength = this.enforcedPolicyOptions.minLength;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.setupSubmitActions())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
|
||||
if (this.kdfConfig == null) {
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
}
|
||||
|
||||
// Create new master key
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
email.trim().toLowerCase(),
|
||||
this.kdfConfig,
|
||||
);
|
||||
const newMasterKeyHash = await this.keyService.hashMasterKey(this.masterPassword, newMasterKey);
|
||||
|
||||
let newProtectedUserKey: [UserKey, EncString] = null;
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
if (userKey == null) {
|
||||
newProtectedUserKey = await this.keyService.makeUserKey(newMasterKey);
|
||||
} else {
|
||||
newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
}
|
||||
|
||||
await this.performSubmitActions(newMasterKeyHash, newMasterKey, newProtectedUserKey);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
// Override in sub-class
|
||||
// Can be used for additional validation and/or other processes the should occur before changing passwords
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
// Override in sub-class
|
||||
}
|
||||
|
||||
async strongPassword(): Promise<boolean> {
|
||||
if (this.masterPassword == null || this.masterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.masterPassword.length < this.minimumLength) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordMinimumlength", this.minimumLength),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.masterPassword !== this.masterPasswordRetype) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPassDoesntMatch"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthResult;
|
||||
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
this.masterPassword,
|
||||
this.enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const weakPassword = strengthResult != null && strengthResult.score < 3;
|
||||
|
||||
if (weakPassword && this.leakedPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (weakPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.leakedPassword) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "exposedMasterPassword" },
|
||||
content: { key: "exposedMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async logOut() {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "logOut" },
|
||||
content: { key: "logOutConfirmation" },
|
||||
acceptButtonText: { key: "logOut" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
}
|
||||
|
||||
getStrengthResult(result: any) {
|
||||
this.passwordStrengthResult = result;
|
||||
}
|
||||
|
||||
getPasswordScoreText(event: PasswordColorText) {
|
||||
this.color = event.color;
|
||||
this.text = event.text;
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import { filter, first, switchMap, tap } from "rxjs/operators";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class SetPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
||||
syncLoading = true;
|
||||
showPassword = false;
|
||||
hint = "";
|
||||
orgSsoIdentifier: string = null;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll = false;
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
successRoute = "vault";
|
||||
activeUserId: UserId;
|
||||
|
||||
forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
ForceSetPasswordReason = ForceSetPasswordReason;
|
||||
|
||||
constructor(
|
||||
protected accountService: AccountService,
|
||||
protected dialogService: DialogService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected messagingService: MessagingService,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: PolicyService,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected syncService: SyncService,
|
||||
protected toastService: ToastService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
super.ngOnInit();
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
this.forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(this.activeUserId),
|
||||
);
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap((qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
return of(qParams.identifier);
|
||||
} else {
|
||||
// Try to get orgSsoId from state as fallback
|
||||
// Note: this is primarily for the TDE user w/out MP obtains admin MP reset permission scenario.
|
||||
return this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeUserId);
|
||||
}
|
||||
}),
|
||||
filter((orgSsoId) => orgSsoId != null),
|
||||
tap((orgSsoId: string) => {
|
||||
this.orgSsoIdentifier = orgSsoId;
|
||||
}),
|
||||
switchMap((orgSsoId: string) => this.organizationApiService.getAutoEnrollStatus(orgSsoId)),
|
||||
tap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) => {
|
||||
this.orgId = orgAutoEnrollStatusResponse.id;
|
||||
this.resetPasswordAutoEnroll = orgAutoEnrollStatusResponse.resetPasswordEnabled;
|
||||
}),
|
||||
switchMap((orgAutoEnrollStatusResponse: OrganizationAutoEnrollStatusResponse) =>
|
||||
// Must get org id from response to get master password policy options
|
||||
this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
|
||||
orgAutoEnrollStatusResponse.id,
|
||||
),
|
||||
),
|
||||
tap((masterPasswordPolicyOptions: MasterPasswordPolicyOptions) => {
|
||||
this.enforcedPolicyOptions = masterPasswordPolicyOptions;
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async setupSubmitActions() {
|
||||
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
let keysRequest: KeysRequest | null = null;
|
||||
let newKeyPair: [string, EncString] | null = null;
|
||||
|
||||
if (
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
// Existing JIT provisioned user in a MP encryption org setting first password
|
||||
// Users in this state will not already have a user asymmetric key pair so must create it for them
|
||||
// We don't want to re-create the user key pair if the user already has one (TDE user case)
|
||||
|
||||
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
|
||||
const existingUserPrivateKey = (await firstValueFrom(
|
||||
this.keyService.userPrivateKey$(this.activeUserId),
|
||||
)) as Uint8Array;
|
||||
const existingUserPublicKey = await firstValueFrom(
|
||||
this.keyService.userPublicKey$(this.activeUserId),
|
||||
);
|
||||
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
|
||||
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
|
||||
newKeyPair = [
|
||||
existingUserPublicKeyB64,
|
||||
await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey[0]),
|
||||
];
|
||||
} else {
|
||||
newKeyPair = await this.keyService.makeKeyPair(userKey[0]);
|
||||
}
|
||||
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
|
||||
}
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
masterPasswordHash,
|
||||
userKey[1].encryptedString,
|
||||
this.hint,
|
||||
this.orgSsoIdentifier,
|
||||
keysRequest,
|
||||
this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions
|
||||
this.kdfConfig.iterations,
|
||||
);
|
||||
try {
|
||||
if (this.resetPasswordAutoEnroll) {
|
||||
this.formPromise = this.masterPasswordApiService
|
||||
.setPassword(request)
|
||||
.then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
return this.organizationApiService.getKeys(this.orgId);
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
this.activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
});
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
protected async onSetPasswordSuccess(
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
keyPair: [string, EncString] | null,
|
||||
) {
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
this.activeUserId,
|
||||
);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(this.activeUserId, this.kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, this.activeUserId);
|
||||
await this.keyService.setUserKey(userKey[0], this.activeUserId);
|
||||
|
||||
// Set private key only for new JIT provisioned users in MP encryption orgs
|
||||
// Existing TDE users will have private key set on sync or on login
|
||||
if (
|
||||
keyPair !== null &&
|
||||
this.forceSetPasswordReason !=
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
|
||||
) {
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, this.activeUserId);
|
||||
}
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.activeUserId);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogRef } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
hint: string;
|
||||
key: string;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
currentMasterPassword: string;
|
||||
|
||||
onSuccessfulChangePassword: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const secret: Verification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: this.currentMasterPassword,
|
||||
};
|
||||
try {
|
||||
await this.userVerificationService.verifyUser(secret);
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
newMasterKeyHash: string,
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
try {
|
||||
// Create Request
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(this.currentMasterPassword),
|
||||
);
|
||||
request.newMasterPasswordHash = newMasterKeyHash;
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
// Update user's password
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("logBackIn"),
|
||||
});
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
|
||||
|
||||
@Directive()
|
||||
export class UpdateTempPasswordComponent extends BaseChangePasswordComponent implements OnInit {
|
||||
hint: string;
|
||||
key: string;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
reason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||
verification: MasterPasswordVerification = {
|
||||
type: VerificationType.MasterPassword,
|
||||
secret: "",
|
||||
};
|
||||
|
||||
onSuccessfulChangePassword: () => Promise<any>;
|
||||
|
||||
get requireCurrentPassword(): boolean {
|
||||
return this.reason === ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private syncService: SyncService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
protected router: Router,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
accountService: AccountService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
accountService,
|
||||
dialogService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordService,
|
||||
messagingService,
|
||||
platformUtilsService,
|
||||
policyService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId));
|
||||
|
||||
// If we somehow end up here without a reason, go back to the home page
|
||||
if (this.reason == ForceSetPasswordReason.None) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await super.ngOnInit();
|
||||
}
|
||||
|
||||
get masterPasswordWarningText(): string {
|
||||
if (this.reason == ForceSetPasswordReason.WeakMasterPassword) {
|
||||
return this.i18nService.t("updateWeakMasterPasswordWarning");
|
||||
} else if (this.reason == ForceSetPasswordReason.TdeOffboarding) {
|
||||
return this.i18nService.t("tdeDisabledMasterPasswordRequired");
|
||||
} else {
|
||||
return this.i18nService.t("updateMasterPasswordWarning");
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword(confirmField: boolean) {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById(confirmField ? "masterPasswordRetype" : "masterPassword").focus();
|
||||
}
|
||||
|
||||
async setupSubmitActions(): Promise<boolean> {
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
this.email = email;
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
// Validation
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.setupSubmitActions())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
this.masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
this.kdfConfig,
|
||||
);
|
||||
const newPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
|
||||
// Grab user key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
|
||||
// Encrypt user key with new master key
|
||||
const newProtectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
userKey,
|
||||
);
|
||||
|
||||
await this.performSubmitActions(newPasswordHash, newMasterKey, newProtectedUserKey);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async performSubmitActions(
|
||||
masterPasswordHash: string,
|
||||
masterKey: MasterKey,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
try {
|
||||
switch (this.reason) {
|
||||
case ForceSetPasswordReason.AdminForcePasswordReset:
|
||||
this.formPromise = this.updateTempPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceSetPasswordReason.WeakMasterPassword:
|
||||
this.formPromise = this.updatePassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
case ForceSetPasswordReason.TdeOffboarding:
|
||||
this.formPromise = this.updateTdeOffboardingPassword(masterPasswordHash, userKey);
|
||||
break;
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedMasterPassword"),
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.None,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (this.onSuccessfulChangePassword != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulChangePassword();
|
||||
} else {
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
private async updateTempPassword(masterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = new UpdateTempPasswordRequest();
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.masterPasswordApiService.putUpdateTempPassword(request);
|
||||
}
|
||||
|
||||
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
const request = await this.userVerificationService.buildRequest(
|
||||
this.verification,
|
||||
PasswordRequest,
|
||||
);
|
||||
request.masterPasswordHint = this.hint;
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = userKey[1].encryptedString;
|
||||
|
||||
return this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
private async updateTdeOffboardingPassword(
|
||||
masterPasswordHash: string,
|
||||
userKey: [UserKey, EncString],
|
||||
) {
|
||||
const request = new UpdateTdeOffboardingPasswordRequest();
|
||||
request.key = userKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
</button>
|
||||
|
||||
<bit-popover [title]="'whatIsADevice' | i18n" #infoPopover>
|
||||
<p>{{ "aDeviceIs" | i18n }}</p>
|
||||
<p class="tw-mb-0">{{ "aDeviceIs" | i18n }}</p>
|
||||
</bit-popover>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,10 +68,7 @@ describe("AuthGuard", () => {
|
||||
{ path: "", component: EmptyComponent },
|
||||
{ path: "guarded-route", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "lock", component: EmptyComponent },
|
||||
{ path: "set-password", component: EmptyComponent },
|
||||
{ path: "set-password-jit", component: EmptyComponent },
|
||||
{ path: "set-initial-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "update-temp-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
{ path: "change-password", component: EmptyComponent },
|
||||
{ path: "remove-password", component: EmptyComponent, canActivate: [authGuard] },
|
||||
]),
|
||||
@@ -125,109 +122,58 @@ describe("AuthGuard", () => {
|
||||
});
|
||||
|
||||
describe("given user is Locked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Locked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
it("should redirect to /set-initial-password when the user has ForceSetPasswordReaason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Locked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toBe("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toBe("/set-initial-password");
|
||||
});
|
||||
|
||||
it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
it("should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.TdeOffboardingUntrustedDevice", async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
false,
|
||||
);
|
||||
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user is Unlocked", () => {
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
describe("given user is Unlocked and ForceSetPasswordReason requires setting an initial password", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
url: "/set-password",
|
||||
},
|
||||
{
|
||||
reason: ForceSetPasswordReason.TdeOffboarding,
|
||||
url: "/update-temp-password",
|
||||
},
|
||||
];
|
||||
describe("given user attempts to navigate to /set-initial-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /set-initial-password when the user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should redirect to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/guarded-route"]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to the set- or update- password route itself", () => {
|
||||
tests.forEach(({ reason, url }) => {
|
||||
it(`should allow navigation to continue to ${url} when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate([url]);
|
||||
expect(router.url).toContain(url);
|
||||
});
|
||||
await router.navigate(["/set-initial-password"]);
|
||||
expect(router.url).toContain("/set-initial-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is ON", () => {
|
||||
describe("given user is Unlocked and ForceSetPasswordReason requires changing an existing password", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
@@ -236,12 +182,7 @@ describe("AuthGuard", () => {
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /change-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(
|
||||
AuthenticationStatus.Unlocked,
|
||||
reason,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason, false);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/change-password");
|
||||
@@ -256,7 +197,6 @@ describe("AuthGuard", () => {
|
||||
AuthenticationStatus.Unlocked,
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
false,
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await router.navigate(["/change-password"]);
|
||||
@@ -265,34 +205,5 @@ describe("AuthGuard", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the PM16117_ChangeExistingPasswordRefactor feature flag is OFF", () => {
|
||||
const tests = [
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
];
|
||||
|
||||
describe("given user attempts to navigate to an auth guarded route", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should redirect to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["guarded-route"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given user attempts to navigate to /update-temp-password", () => {
|
||||
tests.forEach((reason) => {
|
||||
it(`should allow navigation to continue to /update-temp-password when user has ForceSetPasswordReason.${ForceSetPasswordReason[reason]}`, async () => {
|
||||
const { router } = setup(AuthenticationStatus.Unlocked, reason);
|
||||
|
||||
await router.navigate(["/update-temp-password"]);
|
||||
expect(router.url).toContain("/update-temp-password");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export const authGuard: CanActivateFn = async (
|
||||
@@ -30,7 +28,6 @@ export const authGuard: CanActivateFn = async (
|
||||
const keyConnectorService = inject(KeyConnectorService);
|
||||
const accountService = inject(AccountService);
|
||||
const masterPasswordService = inject(MasterPasswordServiceAbstraction);
|
||||
const configService = inject(ConfigService);
|
||||
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
@@ -44,16 +41,11 @@ export const authGuard: CanActivateFn = async (
|
||||
masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
const isSetInitialPasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
// User JIT provisioned into a master-password-encryption org
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked &&
|
||||
forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser &&
|
||||
!routerState.url.includes("set-initial-password") &&
|
||||
isSetInitialPasswordFlagOn
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-initial-password"]);
|
||||
}
|
||||
@@ -62,8 +54,7 @@ export const authGuard: CanActivateFn = async (
|
||||
if (
|
||||
authStatus === AuthenticationStatus.Locked &&
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboardingUntrustedDevice &&
|
||||
!routerState.url.includes("set-initial-password") &&
|
||||
isSetInitialPasswordFlagOn
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
return router.createUrlTree(["/set-initial-password"]);
|
||||
}
|
||||
@@ -90,39 +81,28 @@ export const authGuard: CanActivateFn = async (
|
||||
return router.createUrlTree(["/remove-password"]);
|
||||
}
|
||||
|
||||
// TDE org user has "manage account recovery" permission
|
||||
// Handle cases where a user needs to set a password when they don't already have one:
|
||||
// - TDE org user has been given "manage account recovery" permission
|
||||
// - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password
|
||||
if (
|
||||
forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission &&
|
||||
!routerState.url.includes("set-password") &&
|
||||
(forceSetPasswordReason ===
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding) &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password";
|
||||
const route = "/set-initial-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
// TDE Offboarding on trusted device
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.TdeOffboarding &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("set-initial-password")
|
||||
) {
|
||||
const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/update-temp-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
const isChangePasswordFlagOn = await configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
// Post- Account Recovery or Weak Password on login
|
||||
// Handle cases where a user has a password but needs to set a new one:
|
||||
// - Account recovery
|
||||
// - Weak Password on login
|
||||
if (
|
||||
(forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) &&
|
||||
!routerState.url.includes("update-temp-password") &&
|
||||
!routerState.url.includes("change-password")
|
||||
) {
|
||||
const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password";
|
||||
const route = "/change-password";
|
||||
return router.createUrlTree([route]);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import { lockGuard } from "./lock.guard";
|
||||
interface SetupParams {
|
||||
authStatus: AuthenticationStatus;
|
||||
canLock?: boolean;
|
||||
isLegacyUser?: boolean;
|
||||
clientType?: ClientType;
|
||||
everHadUserKey?: boolean;
|
||||
supportsDeviceTrust?: boolean;
|
||||
@@ -43,7 +42,6 @@ describe("lockGuard", () => {
|
||||
vaultTimeoutSettingsService.canLock.mockResolvedValue(setupParams.canLock);
|
||||
|
||||
const keyService: MockProxy<KeyService> = mock<KeyService>();
|
||||
keyService.isLegacyUser.mockResolvedValue(setupParams.isLegacyUser);
|
||||
keyService.everHadUserKey$.mockReturnValue(of(setupParams.everHadUserKey));
|
||||
|
||||
const platformUtilService: MockProxy<PlatformUtilsService> = mock<PlatformUtilsService>();
|
||||
@@ -155,37 +153,10 @@ describe("lockGuard", () => {
|
||||
expect(router.url).toBe("/");
|
||||
});
|
||||
|
||||
it("should log user out if they are a legacy user on a desktop client", async () => {
|
||||
const { router, messagingService } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Desktop,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should log user out if they are a legacy user on a browser extension client", async () => {
|
||||
const { router, messagingService } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: true,
|
||||
clientType: ClientType.Browser,
|
||||
});
|
||||
|
||||
await router.navigate(["lock"]);
|
||||
expect(router.url).toBe("/");
|
||||
expect(messagingService.send).toHaveBeenCalledWith("logout");
|
||||
});
|
||||
|
||||
it("should allow navigation to the lock route when device trust is supported, the user has a MP, and the user is coming from the login-initiated page", async () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: false,
|
||||
clientType: ClientType.Web,
|
||||
everHadUserKey: false,
|
||||
supportsDeviceTrust: true,
|
||||
@@ -213,7 +184,6 @@ describe("lockGuard", () => {
|
||||
const { router } = setup({
|
||||
authStatus: AuthenticationStatus.Locked,
|
||||
canLock: true,
|
||||
isLegacyUser: false,
|
||||
clientType: ClientType.Web,
|
||||
everHadUserKey: false,
|
||||
supportsDeviceTrust: true,
|
||||
|
||||
@@ -13,7 +13,6 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
/**
|
||||
@@ -31,7 +30,6 @@ export function lockGuard(): CanActivateFn {
|
||||
const authService = inject(AuthService);
|
||||
const keyService = inject(KeyService);
|
||||
const deviceTrustService = inject(DeviceTrustServiceAbstraction);
|
||||
const messagingService = inject(MessagingService);
|
||||
const router = inject(Router);
|
||||
const userVerificationService = inject(UserVerificationService);
|
||||
const vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
@@ -56,11 +54,6 @@ export function lockGuard(): CanActivateFn {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await keyService.isLegacyUser()) {
|
||||
messagingService.send("logout");
|
||||
return false;
|
||||
}
|
||||
|
||||
// User is authN and in locked state.
|
||||
|
||||
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
|
||||
|
||||
@@ -54,7 +54,6 @@ import { UserTypePipe } from "./pipes/user-type.pipe";
|
||||
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
|
||||
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
|
||||
import { I18nPipe } from "./platform/pipes/i18n.pipe";
|
||||
import { PasswordStrengthComponent } from "./tools/password-strength/password-strength.component";
|
||||
import { IconComponent } from "./vault/components/icon.component";
|
||||
|
||||
@NgModule({
|
||||
@@ -108,7 +107,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
TrueFalseValueDirective,
|
||||
LaunchClickDirective,
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
@@ -143,7 +141,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
CopyClickDirective,
|
||||
LaunchClickDirective,
|
||||
UserNamePipe,
|
||||
PasswordStrengthComponent,
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
|
||||
@@ -22,13 +22,11 @@ import {
|
||||
DefaultLoginComponentService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
DefaultRegistrationFinishService,
|
||||
DefaultSetPasswordJitService,
|
||||
DefaultTwoFactorAuthComponentService,
|
||||
DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
LoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
SetPasswordJitService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
@@ -50,8 +48,6 @@ import {
|
||||
LoginSuccessHandlerService,
|
||||
LogoutReason,
|
||||
LogoutService,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
@@ -112,6 +108,7 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||
@@ -154,11 +151,9 @@ import { OrganizationBillingApiService } from "@bitwarden/common/billing/service
|
||||
import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
@@ -169,6 +164,8 @@ import {
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import {
|
||||
SendPasswordService,
|
||||
DefaultSendPasswordService,
|
||||
@@ -238,6 +235,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
|
||||
import {
|
||||
ActiveUserAccessor,
|
||||
ActiveUserStateProvider,
|
||||
DerivedStateProvider,
|
||||
GlobalStateProvider,
|
||||
@@ -535,7 +533,6 @@ const safeProviders: SafeProvider[] = [
|
||||
stateService: StateServiceAbstraction,
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
encryptService: EncryptService,
|
||||
bulkEncryptService: BulkEncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
stateProvider: StateProvider,
|
||||
@@ -552,7 +549,6 @@ const safeProviders: SafeProvider[] = [
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
bulkEncryptService,
|
||||
fileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
@@ -569,7 +565,6 @@ const safeProviders: SafeProvider[] = [
|
||||
StateServiceAbstraction,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
EncryptService,
|
||||
BulkEncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
ConfigService,
|
||||
StateProvider,
|
||||
@@ -976,14 +971,9 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptService,
|
||||
useClass: MultithreadEncryptServiceImplementation,
|
||||
useClass: EncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BulkEncryptService,
|
||||
useClass: BulkEncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EventUploadServiceAbstraction,
|
||||
useClass: EventUploadService,
|
||||
@@ -1030,6 +1020,8 @@ const safeProviders: SafeProvider[] = [
|
||||
KeyGenerationServiceAbstraction,
|
||||
EncryptService,
|
||||
LogService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -1288,10 +1280,15 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultGlobalStateProvider,
|
||||
deps: [StorageServiceProvider, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActiveUserAccessor,
|
||||
useClass: DefaultActiveUserAccessor,
|
||||
deps: [AccountServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ActiveUserStateProvider,
|
||||
useClass: DefaultActiveUserStateProvider,
|
||||
deps: [AccountServiceAbstraction, SingleUserStateProvider],
|
||||
deps: [ActiveUserAccessor, SingleUserStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SingleUserStateProvider,
|
||||
@@ -1424,21 +1421,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultOrganizationInviteService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetPasswordJitService,
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
MasterPasswordApiServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
OrganizationUserApiService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SetInitialPasswordService,
|
||||
useClass: DefaultSetInitialPasswordService,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar {{ color }}"
|
||||
role="progressbar"
|
||||
[ngStyle]="{ width: scoreWidth + '%' }"
|
||||
attr.aria-valuenow="{{ scoreWidth }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<ng-container *ngIf="showText && text">
|
||||
{{ text }}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,118 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnChanges, Output } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
|
||||
export interface PasswordColorText {
|
||||
color: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated July 2024: Use new PasswordStrengthV2Component instead
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-password-strength",
|
||||
templateUrl: "password-strength.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordStrengthComponent implements OnChanges {
|
||||
@Input() showText = false;
|
||||
@Input() email: string;
|
||||
@Input() name: string;
|
||||
@Input() set password(value: string) {
|
||||
this.updatePasswordStrength(value);
|
||||
}
|
||||
@Output() passwordStrengthResult = new EventEmitter<any>();
|
||||
@Output() passwordScoreColor = new EventEmitter<PasswordColorText>();
|
||||
|
||||
masterPasswordScore: number;
|
||||
scoreWidth = 0;
|
||||
color = "bg-danger";
|
||||
text: string;
|
||||
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
|
||||
//used by desktop and browser to display strength text color
|
||||
get masterPasswordScoreColor() {
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
return "success";
|
||||
case 3:
|
||||
return "primary";
|
||||
case 2:
|
||||
return "warning";
|
||||
default:
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
//used by desktop and browser to display strength text
|
||||
get masterPasswordScoreText() {
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
return this.i18nService.t("strong");
|
||||
case 3:
|
||||
return this.i18nService.t("good");
|
||||
case 2:
|
||||
return this.i18nService.t("weak");
|
||||
default:
|
||||
return this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
this.scoreWidth = this.masterPasswordScore == null ? 0 : (this.masterPasswordScore + 1) * 20;
|
||||
|
||||
switch (this.masterPasswordScore) {
|
||||
case 4:
|
||||
this.color = "bg-success";
|
||||
this.text = this.i18nService.t("strong");
|
||||
break;
|
||||
case 3:
|
||||
this.color = "bg-primary";
|
||||
this.text = this.i18nService.t("good");
|
||||
break;
|
||||
case 2:
|
||||
this.color = "bg-warning";
|
||||
this.text = this.i18nService.t("weak");
|
||||
break;
|
||||
default:
|
||||
this.color = "bg-danger";
|
||||
this.text = this.masterPasswordScore != null ? this.i18nService.t("weak") : null;
|
||||
break;
|
||||
}
|
||||
|
||||
this.setPasswordScoreText(this.color, this.text);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updatePasswordStrength(password: string) {
|
||||
const masterPassword = password;
|
||||
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordStrengthService.getPasswordStrength(
|
||||
masterPassword,
|
||||
this.email,
|
||||
this.name?.trim().toLowerCase().split(" "),
|
||||
);
|
||||
this.passwordStrengthResult.emit(strengthResult);
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
}
|
||||
|
||||
setPasswordScoreText(color: string, text: string) {
|
||||
color = color.slice(3);
|
||||
this.passwordScoreColor.emit({ color: color, text: text });
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ export class FolderAddEditComponent implements OnInit {
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId);
|
||||
const folder = await this.folderService.encrypt(this.folder, userKey);
|
||||
this.formPromise = this.folderApiService.save(folder, activeUserId);
|
||||
await this.formPromise;
|
||||
|
||||
@@ -3,12 +3,10 @@ import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, switchMap } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -27,7 +27,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService {
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherListViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasContents = !(ciphers == null || ciphers.length === 0);
|
||||
|
||||
@@ -27,7 +27,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService {
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
this.cipherService.cipherViews$(userId),
|
||||
this.organizationService.organizations$(userId),
|
||||
this.collectionService.decryptedCollections$,
|
||||
this.collectionService.decryptedCollections$(userId),
|
||||
]).pipe(
|
||||
switchMap(([nudgeStatus, ciphers, orgs, collections]) => {
|
||||
const vaultHasMoreThanOneItem = (ciphers?.length ?? 0) > 1;
|
||||
|
||||
@@ -2,13 +2,11 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -109,7 +109,12 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
|
||||
}
|
||||
|
||||
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
|
||||
const storedCollections = await this.collectionService.getAllDecrypted();
|
||||
const storedCollections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
|
||||
),
|
||||
);
|
||||
const orgs = await this.buildOrganizations();
|
||||
const defaulCollectionsFlagEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.CreateDefaultLocation,
|
||||
|
||||
@@ -41,11 +41,6 @@ export * from "./registration/registration-env-selector/registration-env-selecto
|
||||
export * from "./registration/registration-finish/registration-finish.service";
|
||||
export * from "./registration/registration-finish/default-registration-finish.service";
|
||||
|
||||
// set password (JIT user)
|
||||
export * from "./set-password-jit/set-password-jit.component";
|
||||
export * from "./set-password-jit/set-password-jit.service.abstraction";
|
||||
export * from "./set-password-jit/default-set-password-jit.service";
|
||||
|
||||
// user verification
|
||||
export * from "./user-verification/user-verification-dialog.component";
|
||||
export * from "./user-verification/user-verification-dialog.types";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { IsActiveMatchOptions, Router, RouterModule } from "@angular/router";
|
||||
import { Observable, filter, firstValueFrom, map, merge, race, take, timer } from "rxjs";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -185,17 +185,15 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email));
|
||||
const loginEmail$: Observable<string | null> = this.loginEmailService.loginEmail$;
|
||||
|
||||
// Use merge as we want to get the first value from either observable.
|
||||
const firstEmail$ = merge(loginEmail$, activeAccountEmail$).pipe(
|
||||
filter((e): e is string => !!e), // convert null/undefined to false and filter out so we narrow type to string
|
||||
take(1), // complete after first value
|
||||
);
|
||||
let loginEmail: string | undefined = (await firstValueFrom(loginEmail$)) ?? undefined;
|
||||
|
||||
const emailRetrievalTimeout$ = timer(2500).pipe(map(() => undefined as undefined));
|
||||
if (!loginEmail) {
|
||||
loginEmail = (await firstValueFrom(activeAccountEmail$)) ?? undefined;
|
||||
}
|
||||
|
||||
// Wait for either the first email or the timeout to occur so we can proceed
|
||||
// neither above observable will complete, so we have to add a timeout
|
||||
this.email = await firstValueFrom(race(firstEmail$, emailRetrievalTimeout$));
|
||||
this.email = loginEmail;
|
||||
|
||||
if (!this.email) {
|
||||
await this.handleMissingEmail();
|
||||
|
||||
@@ -53,14 +53,14 @@
|
||||
buttonType="secondary"
|
||||
(click)="handleLoginWithPasskeyClick()"
|
||||
>
|
||||
<i class="bwi bwi-passkey tw-mr-1"></i>
|
||||
<i class="bwi bwi-passkey tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "logInWithPasskey" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<!-- Button to Login with SSO -->
|
||||
<button type="button" bitButton block buttonType="secondary" (click)="handleSsoClick()">
|
||||
<i class="bwi bwi-provider tw-mr-1"></i>
|
||||
<i class="bwi bwi-provider tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "useSingleSignOn" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@
|
||||
buttonType="secondary"
|
||||
(click)="startAuthRequestLogin()"
|
||||
>
|
||||
<i class="bwi bwi-mobile"></i>
|
||||
<i class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||
{{ "loginWithDevice" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -18,7 +18,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
@@ -230,29 +229,21 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
let credentials: PasswordLoginCredentials;
|
||||
// Try to retrieve any org policies from an org invite now so we can send it to the
|
||||
// login strategies. Since it is optional and we only want to be doing this on the
|
||||
// web we will only send in content in the right context.
|
||||
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
|
||||
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
|
||||
: null;
|
||||
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
// Try to retrieve any org policies from an org invite now so we can send it to the
|
||||
// login strategies. Since it is optional and we only want to be doing this on the
|
||||
// web we will only send in content in the right context.
|
||||
const orgPoliciesFromInvite = this.loginComponentService.getOrgPoliciesFromOrgInvite
|
||||
? await this.loginComponentService.getOrgPoliciesFromOrgInvite()
|
||||
: null;
|
||||
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
|
||||
|
||||
const orgMasterPasswordPolicyOptions = orgPoliciesFromInvite?.enforcedPasswordPolicyOptions;
|
||||
|
||||
credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
undefined,
|
||||
orgMasterPasswordPolicyOptions,
|
||||
);
|
||||
} else {
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
}
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
email,
|
||||
masterPassword,
|
||||
undefined,
|
||||
orgMasterPasswordPolicyOptions,
|
||||
);
|
||||
|
||||
try {
|
||||
const authResult = await this.loginStrategyService.logIn(credentials);
|
||||
@@ -332,7 +323,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// Determine where to send the user next
|
||||
// The AuthGuard will handle routing to update-temp-password based on state
|
||||
// The AuthGuard will handle routing to change-password based on state
|
||||
|
||||
// TODO: PM-18269 - evaluate if we can combine this with the
|
||||
// password evaluation done in the password login strategy.
|
||||
@@ -344,7 +335,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (orgPolicies) {
|
||||
// Since we have retrieved the policies, we can go ahead and set them into state for future use
|
||||
// e.g., the update-password page currently only references state for policy data and
|
||||
// e.g., the change-password page currently only references state for policy data and
|
||||
// doesn't fallback to pulling them from the server like it should if they are null.
|
||||
await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies);
|
||||
|
||||
@@ -352,13 +343,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
orgPolicies.enforcedPasswordPolicyOptions,
|
||||
);
|
||||
if (isPasswordChangeRequired) {
|
||||
const changePasswordFeatureFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
await this.router.navigate(
|
||||
changePasswordFeatureFlagOn ? ["change-password"] : ["update-password"],
|
||||
);
|
||||
await this.router.navigate(["change-password"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -151,25 +150,17 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// TODO: PM-22663 use the new service to handle routing.
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
|
||||
);
|
||||
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(activeUserId),
|
||||
);
|
||||
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
await this.router.navigate(["/change-password"]);
|
||||
} else {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
await this.router.navigate(["/change-password"]);
|
||||
} else {
|
||||
await this.router.navigate(["/vault"]);
|
||||
}
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { DefaultSetPasswordJitService } from "./default-set-password-jit.service";
|
||||
import { SetPasswordCredentials } from "./set-password-jit.service.abstraction";
|
||||
|
||||
describe("DefaultSetPasswordJitService", () => {
|
||||
let sut: DefaultSetPasswordJitService;
|
||||
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
|
||||
beforeEach(() => {
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
|
||||
sut = new DefaultSetPasswordJitService(
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should instantiate the DefaultSetPasswordJitService", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setPassword", () => {
|
||||
let masterKey: MasterKey;
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let protectedUserKey: [UserKey, EncString];
|
||||
let keyPair: [string, EncString];
|
||||
let keysRequest: KeysRequest;
|
||||
let organizationKeys: OrganizationKeysResponse;
|
||||
let orgPublicKey: Uint8Array;
|
||||
|
||||
let orgSsoIdentifier: string;
|
||||
let orgId: string;
|
||||
let resetPasswordAutoEnroll: boolean;
|
||||
let userId: UserId;
|
||||
let passwordInputResult: PasswordInputResult;
|
||||
let credentials: SetPasswordCredentials;
|
||||
|
||||
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||
let setPasswordRequest: SetPasswordRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
userKeyEncString = new EncString("userKeyEncrypted");
|
||||
protectedUserKey = [userKey, userKeyEncString];
|
||||
keyPair = ["publicKey", new EncString("privateKey")];
|
||||
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
organizationKeys = {
|
||||
privateKey: "orgPrivateKey",
|
||||
publicKey: "orgPublicKey",
|
||||
} as OrganizationKeysResponse;
|
||||
orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
|
||||
orgSsoIdentifier = "orgSsoIdentifier";
|
||||
orgId = "orgId";
|
||||
resetPasswordAutoEnroll = false;
|
||||
userId = "userId" as UserId;
|
||||
|
||||
passwordInputResult = {
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
userId,
|
||||
};
|
||||
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(null);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
passwordInputResult.kdfConfig.iterations,
|
||||
);
|
||||
});
|
||||
|
||||
function setupSetPasswordMocks(hasUserKey = true) {
|
||||
if (!hasUserKey) {
|
||||
keyService.userKey$.mockReturnValue(of(null));
|
||||
keyService.makeUserKey.mockResolvedValue(protectedUserKey);
|
||||
} else {
|
||||
keyService.userKey$.mockReturnValue(of(userKey));
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey);
|
||||
}
|
||||
|
||||
keyService.makeKeyPair.mockResolvedValue(keyPair);
|
||||
|
||||
masterPasswordApiService.setPassword.mockResolvedValue(undefined);
|
||||
masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined);
|
||||
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined);
|
||||
kdfConfigService.setKdfConfig.mockResolvedValue(undefined);
|
||||
keyService.setUserKey.mockResolvedValue(undefined);
|
||||
|
||||
keyService.setPrivateKey.mockResolvedValue(undefined);
|
||||
|
||||
masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
function setupResetPasswordAutoEnrollMocks(organizationKeysExist = true) {
|
||||
if (organizationKeysExist) {
|
||||
organizationApiService.getKeys.mockResolvedValue(organizationKeys);
|
||||
} else {
|
||||
organizationApiService.getKeys.mockResolvedValue(null);
|
||||
return;
|
||||
}
|
||||
|
||||
keyService.userKey$.mockReturnValue(of(userKey));
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(userKeyEncString);
|
||||
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
it("should set password successfully (given a user key)", async () => {
|
||||
// Arrange
|
||||
setupSetPasswordMocks();
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should set password successfully (given no user key)", async () => {
|
||||
// Arrange
|
||||
setupSetPasswordMocks(false);
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should handle reset password auto enroll", async () => {
|
||||
// Arrange
|
||||
credentials.resetPasswordAutoEnroll = true;
|
||||
|
||||
setupSetPasswordMocks();
|
||||
setupResetPasswordAutoEnrollMocks();
|
||||
|
||||
// Act
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(userKey, orgPublicKey);
|
||||
expect(
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when handling reset password auto enroll, it should throw an error if organization keys are not found", async () => {
|
||||
// Arrange
|
||||
credentials.resetPasswordAutoEnroll = true;
|
||||
|
||||
setupSetPasswordMocks();
|
||||
setupResetPasswordAutoEnrollMocks(false);
|
||||
|
||||
// Act and Assert
|
||||
await expect(sut.setPassword(credentials)).rejects.toThrow();
|
||||
expect(
|
||||
organizationUserApiService.putOrganizationUserResetPasswordEnrollment,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "./set-password-jit.service.abstraction";
|
||||
|
||||
export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
constructor(
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
userId,
|
||||
} = credentials;
|
||||
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
if (value == null) {
|
||||
throw new Error(`${key} not found. Could not set password.`);
|
||||
}
|
||||
}
|
||||
|
||||
const protectedUserKey = await this.makeProtectedUserKey(newMasterKey, userId);
|
||||
if (protectedUserKey == null) {
|
||||
throw new Error("protectedUserKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
// Since this is an existing JIT provisioned user in a MP encryption org setting first password,
|
||||
// they will not already have a user asymmetric key pair so we must create it for them.
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
await this.masterPasswordApiService.setPassword(request);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
await this.updateAccountDecryptionProperties(newMasterKey, kdfConfig, protectedUserKey, userId);
|
||||
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async makeProtectedUserKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
): Promise<[UserKey, EncString]> {
|
||||
let protectedUserKey: [UserKey, EncString] = null;
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
protectedUserKey = await this.keyService.makeUserKey(masterKey);
|
||||
} else {
|
||||
protectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
|
||||
}
|
||||
|
||||
return protectedUserKey;
|
||||
}
|
||||
|
||||
private async makeKeyPairAndRequest(
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
): Promise<[[string, EncString], KeysRequest]> {
|
||||
const keyPair = await this.keyService.makeKeyPair(protectedUserKey[0]);
|
||||
if (keyPair == null) {
|
||||
throw new Error("keyPair not found. Could not set password.");
|
||||
}
|
||||
const keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
|
||||
return [keyPair, keysRequest];
|
||||
}
|
||||
|
||||
private async updateAccountDecryptionProperties(
|
||||
masterKey: MasterKey,
|
||||
kdfConfig: KdfConfig,
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
userId: UserId,
|
||||
) {
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(protectedUserKey[0], userId);
|
||||
}
|
||||
|
||||
private async handleResetPasswordAutoEnroll(
|
||||
masterKeyHash: string,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) {
|
||||
const organizationKeys = await this.organizationApiService.getKeys(orgId);
|
||||
|
||||
if (organizationKeys == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization public key
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
throw new Error("userKey not found. Could not handle reset password auto enroll.");
|
||||
}
|
||||
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.masterPasswordHash = masterKeyHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
orgId,
|
||||
userId,
|
||||
resetRequest,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<ng-container *ngIf="syncLoading">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-mr-2" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!syncLoading">
|
||||
<app-callout
|
||||
type="warning"
|
||||
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
|
||||
*ngIf="resetPasswordAutoEnroll"
|
||||
>
|
||||
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
||||
@@ -1,135 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ToastService } from "../../../../components/src/toast";
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
SetPasswordJitService,
|
||||
} from "./set-password-jit.service.abstraction";
|
||||
|
||||
@Component({
|
||||
selector: "auth-set-password-jit",
|
||||
templateUrl: "set-password-jit.component.html",
|
||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||
})
|
||||
export class SetPasswordJitComponent implements OnInit {
|
||||
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
|
||||
protected email: string;
|
||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
protected orgId: string;
|
||||
protected orgSsoIdentifier: string;
|
||||
protected resetPasswordAutoEnroll: boolean;
|
||||
protected submitting = false;
|
||||
protected syncLoading = true;
|
||||
protected userId: UserId;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private i18nService: I18nService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private router: Router,
|
||||
private setPasswordJitService: SetPasswordJitService,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = activeAccount?.id;
|
||||
this.email = activeAccount?.email;
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
|
||||
await this.handleQueryParams();
|
||||
}
|
||||
|
||||
private async handleQueryParams() {
|
||||
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
|
||||
if (qParams.identifier != null) {
|
||||
try {
|
||||
this.orgSsoIdentifier = qParams.identifier;
|
||||
|
||||
const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus(
|
||||
this.orgSsoIdentifier,
|
||||
);
|
||||
this.orgId = autoEnrollStatus.id;
|
||||
this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled;
|
||||
this.masterPasswordPolicyOptions =
|
||||
await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(autoEnrollStatus.id);
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
const credentials: SetPasswordCredentials = {
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
userId: this.userId,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.setPasswordJitService.setPassword(credentials);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
this.submitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||
});
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("inviteAccepted"),
|
||||
});
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
await this.router.navigate(["vault"]);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPasswordHint: string;
|
||||
kdfConfig: KdfConfig;
|
||||
orgSsoIdentifier: string;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
userId: UserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service handles setting a password for a "just-in-time" provisioned user.
|
||||
*
|
||||
* A "just-in-time" (JIT) provisioned user is a user who does not have a registered account at the
|
||||
* time they first click "Login with SSO". Once they click "Login with SSO" we register the account on
|
||||
* the fly ("just-in-time").
|
||||
*/
|
||||
export abstract class SetPasswordJitService {
|
||||
/**
|
||||
* Sets the password for a JIT provisioned user.
|
||||
*
|
||||
* @param credentials An object of the credentials needed to set the password for a JIT provisioned user
|
||||
* @throws If any property on the `credentials` object is null or undefined, or if a protectedUserKey
|
||||
* or newKeyPair could not be created.
|
||||
*/
|
||||
abstract setPassword(credentials: SetPasswordCredentials): Promise<void>;
|
||||
}
|
||||
@@ -23,12 +23,10 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -118,7 +116,6 @@ export class SsoComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private ssoComponentService: SsoComponentService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
|
||||
@@ -534,11 +531,7 @@ export class SsoComponent implements OnInit {
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string) {
|
||||
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password-jit";
|
||||
|
||||
const route = "set-initial-password";
|
||||
await this.router.navigate([route], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, Router, convertToParamMap } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
@@ -24,8 +24,10 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@@ -66,7 +68,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
let mockLoginEmailService: MockProxy<LoginEmailServiceAbstraction>;
|
||||
let mockUserDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let mockMasterPasswordService: FakeMasterPasswordService;
|
||||
let mockMasterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let mockAccountService: FakeAccountService;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
@@ -107,7 +109,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
mockUserDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockAccountService = mockAccountServiceWith(userId);
|
||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||
mockMasterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockToastService = mock<ToastService>();
|
||||
mockTwoFactorAuthCompService = mock<TwoFactorAuthComponentService>();
|
||||
@@ -212,6 +214,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
},
|
||||
{ provide: AuthService, useValue: mockAuthService },
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: MasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -267,54 +270,16 @@ describe("TwoFactorAuthComponent", () => {
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword);
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
it("navigates to the /set-password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
|
||||
it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => {
|
||||
// Arrange
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
await component.submit(token, remember);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
|
||||
// Assert
|
||||
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
@@ -322,21 +287,19 @@ describe("TwoFactorAuthComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => {
|
||||
it("does not navigate to the /set-password route when the user has key connector even if user has no master password", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector,
|
||||
);
|
||||
|
||||
await component.submit(token, remember);
|
||||
await component.submit(token, remember);
|
||||
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -344,6 +307,9 @@ describe("TwoFactorAuthComponent", () => {
|
||||
it("navigates to the component's defined success route (vault is default) when the login is successful", async () => {
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
mockAuthService.activeAccountStatus$ = new BehaviorSubject(AuthenticationStatus.Unlocked);
|
||||
mockMasterPasswordService.forceSetPasswordReason$.mockReturnValue(
|
||||
of(ForceSetPasswordReason.None),
|
||||
);
|
||||
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
@@ -409,7 +375,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
await component.submit(token, remember);
|
||||
|
||||
// Assert
|
||||
expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
expect(mockMasterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
userId,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
TrustedDeviceUserDecryptionOption,
|
||||
UserDecryptionOptions,
|
||||
@@ -32,9 +31,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -156,7 +153,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
private twoFactorService: TwoFactorService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
@@ -171,7 +167,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService,
|
||||
private authService: AuthService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -507,19 +502,15 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
// TODO: PM-22663 use the new service to handle routing.
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
) {
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
const forceSetPasswordReason = await firstValueFrom(
|
||||
this.masterPasswordService.forceSetPasswordReason$(userId),
|
||||
);
|
||||
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
return "change-password";
|
||||
}
|
||||
if (
|
||||
forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword ||
|
||||
forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset
|
||||
) {
|
||||
return "change-password";
|
||||
}
|
||||
|
||||
return "vault";
|
||||
@@ -575,11 +566,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async handleChangePasswordRequired(orgIdentifier: string | undefined) {
|
||||
const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password";
|
||||
|
||||
const route = "set-initial-password";
|
||||
await this.router.navigate([route], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./auth-request-api.service";
|
||||
export * from "./pin.service.abstraction";
|
||||
export * from "./login-email.service";
|
||||
export * from "./login-strategy.service";
|
||||
export * from "./user-decryption-options.service.abstraction";
|
||||
|
||||
@@ -327,6 +327,7 @@ describe("LoginStrategy", () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
keyService.getUserKey.mockResolvedValue(userKey);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
@@ -343,6 +344,15 @@ describe("LoginStrategy", () => {
|
||||
|
||||
expect(apiService.postAccountKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if userKey is CoseEncrypt0 (V2 encryption) in createKeyPairForOldAccount", async () => {
|
||||
keyService.getUserKey.mockResolvedValue({
|
||||
inner: () => ({ type: 7 }),
|
||||
} as UserKey);
|
||||
await expect(passwordLoginStrategy["createKeyPairForOldAccount"](userId)).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Two-factor authentication", () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
@@ -325,7 +326,11 @@ export abstract class LoginStrategy {
|
||||
|
||||
protected async createKeyPairForOldAccount(userId: UserId) {
|
||||
try {
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(userId);
|
||||
const userKey = await this.keyService.getUserKey(userId);
|
||||
if (userKey.inner().type == EncryptionType.CoseEncrypt0) {
|
||||
throw new Error("Cannot create key pair for account on V2 encryption");
|
||||
}
|
||||
|
||||
const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey);
|
||||
if (!privateKey.encryptedString) {
|
||||
throw new Error("Failed to create encrypted private key");
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
@@ -221,7 +220,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when it meets requirements", async () => {
|
||||
@@ -230,7 +232,10 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).not.toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("when given master password policies as part of the login credentials from an org invite, it combines them with the token response policies to evaluate the user's password as weak", async () => {
|
||||
@@ -242,12 +247,6 @@ describe("PasswordLoginStrategy", () => {
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
jest
|
||||
.spyOn(configService, "getFeatureFlag")
|
||||
.mockImplementation((flag: FeatureFlag) =>
|
||||
Promise.resolve(flag === FeatureFlag.PM16117_ChangeExistingPasswordRefactor),
|
||||
);
|
||||
|
||||
credentials.masterPasswordPoliciesFromOrgInvite = Object.assign(
|
||||
new MasterPasswordPolicyOptions(),
|
||||
{
|
||||
@@ -296,9 +295,16 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
|
||||
enforceOnLogin: true,
|
||||
});
|
||||
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
|
||||
combinedMasterPasswordPolicyOptions,
|
||||
);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
@@ -330,9 +336,16 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
const combinedMasterPasswordPolicyOptions = Object.assign(new MasterPasswordPolicyOptions(), {
|
||||
enforceOnLogin: true,
|
||||
});
|
||||
policyService.combineMasterPasswordPolicyOptions.mockReturnValue(
|
||||
combinedMasterPasswordPolicyOptions,
|
||||
);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
|
||||
const token2FAResponse = new IdentityTwoFactorResponse({
|
||||
TwoFactorProviders: ["0"],
|
||||
TwoFactorProviders2: { 0: null },
|
||||
|
||||
@@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@@ -171,35 +170,22 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
return;
|
||||
}
|
||||
|
||||
// The identity result can contain master password policies for the user's organizations
|
||||
let masterPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
|
||||
// The identity result can contain master password policies for the user's organizations.
|
||||
// Get the master password policy options from both the org invite and the identity response.
|
||||
const masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
|
||||
credentials.masterPasswordPoliciesFromOrgInvite,
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
|
||||
);
|
||||
|
||||
// We deliberately do not check enforceOnLogin as existing users who are logging
|
||||
// in after getting an org invite should always be forced to set a password that
|
||||
// meets the org's policy. Org Invite -> Registration also works this way for
|
||||
// new BW users as well.
|
||||
if (
|
||||
await this.configService.getFeatureFlag(FeatureFlag.PM16117_ChangeExistingPasswordRefactor)
|
||||
!credentials.masterPasswordPoliciesFromOrgInvite &&
|
||||
!masterPasswordPolicyOptions?.enforceOnLogin
|
||||
) {
|
||||
// Get the master password policy options from both the org invite and the identity response.
|
||||
masterPasswordPolicyOptions = this.policyService.combineMasterPasswordPolicyOptions(
|
||||
credentials.masterPasswordPoliciesFromOrgInvite,
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse),
|
||||
);
|
||||
|
||||
// We deliberately do not check enforceOnLogin as existing users who are logging
|
||||
// in after getting an org invite should always be forced to set a password that
|
||||
// meets the org's policy. Org Invite -> Registration also works this way for
|
||||
// new BW users as well.
|
||||
if (
|
||||
!credentials.masterPasswordPoliciesFromOrgInvite &&
|
||||
!masterPasswordPolicyOptions?.enforceOnLogin
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a policy active, evaluate the supplied password before its no longer in memory
|
||||
|
||||
@@ -10,7 +10,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
@@ -83,6 +82,7 @@ describe("SsoLoginStrategy", () => {
|
||||
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
|
||||
const ssoRedirectUrl = "SSO_REDIRECT_URL";
|
||||
const ssoOrgId = "SSO_ORG_ID";
|
||||
const privateKey = "userKeyEncryptedPrivateKey";
|
||||
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
@@ -114,6 +114,9 @@ describe("SsoLoginStrategy", () => {
|
||||
tokenService.decodeAccessToken.mockResolvedValue({
|
||||
sub: userId,
|
||||
});
|
||||
keyService.userEncryptedPrivateKey$
|
||||
.calledWith(userId)
|
||||
.mockReturnValue(of(privateKey as EncryptedString));
|
||||
|
||||
const mockVaultTimeoutAction = VaultTimeoutAction.Lock;
|
||||
const mockVaultTimeoutActionBSub = new BehaviorSubject<VaultTimeoutAction>(
|
||||
@@ -163,6 +166,7 @@ describe("SsoLoginStrategy", () => {
|
||||
|
||||
it("sends SSO information to server", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
keyService.hasUserKey.mockResolvedValue(true);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
@@ -185,6 +189,7 @@ describe("SsoLoginStrategy", () => {
|
||||
it("does not set keys for new SSO user flow", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
tokenResponse.privateKey = null;
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
@@ -210,42 +215,28 @@ describe("SsoLoginStrategy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag.mockImplementation(async (flag) => {
|
||||
if (flag === FeatureFlag.PM16117_SetInitialPasswordRefactor) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
|
||||
it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
|
||||
// Arrange
|
||||
const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: false,
|
||||
TrustedDeviceOption: null,
|
||||
KeyConnectorOption: null,
|
||||
};
|
||||
const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
describe("given the user does not have the `trustedDeviceOption`, does not have a master password, is not using key connector, does not have a user key, but they DO have a `userKeyEncryptedPrivateKey`", () => {
|
||||
it("should set the forceSetPasswordReason to TdeOffboardingUntrustedDevice", async () => {
|
||||
// Arrange
|
||||
const mockUserDecryptionOptions: IUserDecryptionOptionsServerResponse = {
|
||||
HasMasterPassword: false,
|
||||
TrustedDeviceOption: null,
|
||||
KeyConnectorOption: null,
|
||||
};
|
||||
const tokenResponse = identityTokenResponseFactory(null, mockUserDecryptionOptions);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
keyService.hasUserKey.mockResolvedValue(false);
|
||||
|
||||
keyService.userEncryptedPrivateKey$.mockReturnValue(
|
||||
of("userKeyEncryptedPrivateKey" as EncryptedString),
|
||||
);
|
||||
keyService.hasUserKey.mockResolvedValue(false);
|
||||
// Act
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
// Act
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
// Assert
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledTimes(1);
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
@@ -344,38 +343,18 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
);
|
||||
|
||||
if (isSetInitialPasswordFlagOn) {
|
||||
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
|
||||
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
|
||||
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
|
||||
// and so we don't want them falling into the createKeyPairForOldAccount flow
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
} else if (tokenResponse.privateKey) {
|
||||
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
|
||||
// This is just existing TDE users or a TDE offboarder on an untrusted device
|
||||
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
|
||||
}
|
||||
// else {
|
||||
// User could be new JIT provisioned SSO user in either a MP encryption org OR a TDE org.
|
||||
// In either case, the user doesn't yet have a user asymmetric key pair, a user key, or a master key + master key encrypted user key.
|
||||
// }
|
||||
} else {
|
||||
// A user that does not yet have a masterKeyEncryptedUserKey set is a new SSO user
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (!newSsoUser) {
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
if (tokenResponse.hasMasterKeyEncryptedUserKey()) {
|
||||
// User has masterKeyEncryptedUserKey, so set the userKeyEncryptedPrivateKey
|
||||
// Note: new JIT provisioned SSO users will not yet have a user asymmetric key pair
|
||||
// and so we don't want them falling into the createKeyPairForOldAccount flow
|
||||
await this.keyService.setPrivateKey(
|
||||
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
|
||||
userId,
|
||||
);
|
||||
} else if (tokenResponse.privateKey) {
|
||||
// User doesn't have masterKeyEncryptedUserKey but they do have a userKeyEncryptedPrivateKey
|
||||
// This is just existing TDE users or a TDE offboarder on an untrusted device
|
||||
await this.keyService.setPrivateKey(tokenResponse.privateKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,30 +410,25 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
// - UserDecryptionOptions.UsesKeyConnector is undefined. -- they aren't using key connector
|
||||
// - UserKey is not set after successful login -- because automatic decryption is not available
|
||||
// - userKeyEncryptedPrivateKey is set after successful login -- this is the key differentiator between a TDE org user logging into an untrusted device and MP encryption JIT provisioned user logging in for the first time.
|
||||
const isSetInitialPasswordFlagOn = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_SetInitialPasswordRefactor,
|
||||
// Why is that the case? Because we set the userKeyEncryptedPrivateKey when we create the userKey, and this is serving as a proxy to tell us that the userKey has been created already (when enrolling in TDE).
|
||||
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
|
||||
this.keyService.userEncryptedPrivateKey$(userId),
|
||||
);
|
||||
const hasUserKey = await this.keyService.hasUserKey(userId);
|
||||
|
||||
if (isSetInitialPasswordFlagOn) {
|
||||
const hasUserKeyEncryptedPrivateKey = await firstValueFrom(
|
||||
this.keyService.userEncryptedPrivateKey$(userId),
|
||||
// TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
|
||||
if (
|
||||
!userDecryptionOptions.trustedDeviceOption &&
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
|
||||
hasUserKeyEncryptedPrivateKey &&
|
||||
!hasUserKey
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
const hasUserKey = await this.keyService.hasUserKey(userId);
|
||||
|
||||
// TODO: PM-23491 we should explore consolidating this logic into a flag on the server. It could be set when an org is switched from TDE to MP encryption for each org user.
|
||||
if (
|
||||
!userDecryptionOptions.trustedDeviceOption &&
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
|
||||
hasUserKeyEncryptedPrivateKey &&
|
||||
!hasUserKey
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeOffboardingUntrustedDevice,
|
||||
userId,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has permission to set password but hasn't yet
|
||||
|
||||
@@ -86,9 +86,6 @@ describe("AuthRequestService", () => {
|
||||
|
||||
describe("approveOrDenyAuthRequest", () => {
|
||||
beforeEach(() => {
|
||||
encryptService.rsaEncrypt.mockResolvedValue({
|
||||
encryptedString: "ENCRYPTED_STRING",
|
||||
} as EncString);
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue({
|
||||
encryptedString: "ENCRYPTED_STRING",
|
||||
} as EncString);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./pin/pin.service.implementation";
|
||||
export * from "./login-email/login-email.service";
|
||||
export * from "./login-strategies/login-strategy.service";
|
||||
export * from "./user-decryption-options/user-decryption-options.service";
|
||||
|
||||
5
libs/client-type/README.md
Normal file
5
libs/client-type/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# client-type
|
||||
|
||||
Owned by: platform
|
||||
|
||||
Exports the ClientType enum
|
||||
3
libs/client-type/eslint.config.mjs
Normal file
3
libs/client-type/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "../../eslint.config.mjs";
|
||||
|
||||
export default [...baseConfig];
|
||||
10
libs/client-type/jest.config.js
Normal file
10
libs/client-type/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
displayName: "client-type",
|
||||
preset: "../../jest.preset.js",
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js", "html"],
|
||||
coverageDirectory: "../../coverage/libs/client-type",
|
||||
};
|
||||
11
libs/client-type/package.json
Normal file
11
libs/client-type/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@bitwarden/client-type",
|
||||
"version": "0.0.1",
|
||||
"description": "Exports the ClientType enum",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "GPL-3.0",
|
||||
"author": "platform"
|
||||
}
|
||||
33
libs/client-type/project.json
Normal file
33
libs/client-type/project.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "client-type",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/client-type/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/client-type",
|
||||
"main": "libs/client-type/src/index.ts",
|
||||
"tsConfig": "libs/client-type/tsconfig.lib.json",
|
||||
"assets": ["libs/client-type/*.md"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/client-type/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/client-type/jest.config.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
libs/client-type/src/client-type.spec.ts
Normal file
8
libs/client-type/src/client-type.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as lib from "./index";
|
||||
|
||||
describe("client-type", () => {
|
||||
// This test will fail until something is exported from index.ts
|
||||
it("should work", () => {
|
||||
expect(lib).toBeDefined();
|
||||
});
|
||||
});
|
||||
10
libs/client-type/src/index.ts
Normal file
10
libs/client-type/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum ClientType {
|
||||
Web = "web",
|
||||
Browser = "browser",
|
||||
Desktop = "desktop",
|
||||
// Mobile = "mobile",
|
||||
Cli = "cli",
|
||||
// DirectoryConnector = "connector",
|
||||
}
|
||||
6
libs/client-type/tsconfig.eslint.json
Normal file
6
libs/client-type/tsconfig.eslint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": ["src/**/*.ts", "src/**/*.js"],
|
||||
"exclude": ["**/build", "**/dist"]
|
||||
}
|
||||
13
libs/client-type/tsconfig.json
Normal file
13
libs/client-type/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
libs/client-type/tsconfig.lib.json
Normal file
10
libs/client-type/tsconfig.lib.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
|
||||
}
|
||||
10
libs/client-type/tsconfig.spec.json
Normal file
10
libs/client-type/tsconfig.spec.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
@@ -1,309 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { Observable, map, of, switchMap, take } from "rxjs";
|
||||
|
||||
import {
|
||||
GlobalState,
|
||||
GlobalStateProvider,
|
||||
KeyDefinition,
|
||||
ActiveUserState,
|
||||
SingleUserState,
|
||||
SingleUserStateProvider,
|
||||
StateProvider,
|
||||
ActiveUserStateProvider,
|
||||
DerivedState,
|
||||
DeriveDefinition,
|
||||
DerivedStateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../src/platform/state";
|
||||
import { UserId } from "../src/types/guid";
|
||||
import { DerivedStateDependencies } from "../src/types/state";
|
||||
|
||||
import { FakeAccountService } from "./fake-account-service";
|
||||
import {
|
||||
FakeActiveUserState,
|
||||
FakeDerivedState,
|
||||
FakeGlobalState,
|
||||
FakeSingleUserState,
|
||||
} from "./fake-state";
|
||||
|
||||
export class FakeGlobalStateProvider implements GlobalStateProvider {
|
||||
mock = mock<GlobalStateProvider>();
|
||||
establishedMocks: Map<string, FakeGlobalState<unknown>> = new Map();
|
||||
states: Map<string, GlobalState<unknown>> = new Map();
|
||||
get<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
||||
this.mock.get(keyDefinition);
|
||||
const cacheKey = this.cacheKey(keyDefinition);
|
||||
let result = this.states.get(cacheKey);
|
||||
|
||||
if (result == null) {
|
||||
let fake: FakeGlobalState<T>;
|
||||
// Look for established mock
|
||||
if (this.establishedMocks.has(keyDefinition.key)) {
|
||||
fake = this.establishedMocks.get(keyDefinition.key) as FakeGlobalState<T>;
|
||||
} else {
|
||||
fake = new FakeGlobalState<T>();
|
||||
}
|
||||
fake.keyDefinition = keyDefinition;
|
||||
result = fake;
|
||||
this.states.set(cacheKey, result);
|
||||
|
||||
result = new FakeGlobalState<T>();
|
||||
this.states.set(cacheKey, result);
|
||||
}
|
||||
return result as GlobalState<T>;
|
||||
}
|
||||
|
||||
private cacheKey(keyDefinition: KeyDefinition<unknown>) {
|
||||
return `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
|
||||
}
|
||||
|
||||
getFake<T>(keyDefinition: KeyDefinition<T>): FakeGlobalState<T> {
|
||||
return this.get(keyDefinition) as FakeGlobalState<T>;
|
||||
}
|
||||
|
||||
mockFor<T>(keyDefinition: KeyDefinition<T>, initialValue?: T): FakeGlobalState<T> {
|
||||
const cacheKey = this.cacheKey(keyDefinition);
|
||||
if (!this.states.has(cacheKey)) {
|
||||
this.states.set(cacheKey, new FakeGlobalState<T>(initialValue));
|
||||
}
|
||||
return this.states.get(cacheKey) as FakeGlobalState<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
||||
mock = mock<SingleUserStateProvider>();
|
||||
states: Map<string, SingleUserState<unknown>> = new Map();
|
||||
|
||||
constructor(
|
||||
readonly updateSyncCallback?: (
|
||||
key: UserKeyDefinition<unknown>,
|
||||
userId: UserId,
|
||||
newValue: unknown,
|
||||
) => Promise<void>,
|
||||
) {}
|
||||
|
||||
get<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
|
||||
this.mock.get(userId, userKeyDefinition);
|
||||
const cacheKey = this.cacheKey(userId, userKeyDefinition);
|
||||
let result = this.states.get(cacheKey);
|
||||
|
||||
if (result == null) {
|
||||
result = this.buildFakeState(userId, userKeyDefinition);
|
||||
this.states.set(cacheKey, result);
|
||||
}
|
||||
return result as SingleUserState<T>;
|
||||
}
|
||||
|
||||
getFake<T>(
|
||||
userId: UserId,
|
||||
userKeyDefinition: UserKeyDefinition<T>,
|
||||
{ allowInit }: { allowInit: boolean } = { allowInit: true },
|
||||
): FakeSingleUserState<T> {
|
||||
if (!allowInit && this.states.get(this.cacheKey(userId, userKeyDefinition)) == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.get(userId, userKeyDefinition) as FakeSingleUserState<T>;
|
||||
}
|
||||
|
||||
mockFor<T>(
|
||||
userId: UserId,
|
||||
userKeyDefinition: UserKeyDefinition<T>,
|
||||
initialValue?: T,
|
||||
): FakeSingleUserState<T> {
|
||||
const cacheKey = this.cacheKey(userId, userKeyDefinition);
|
||||
if (!this.states.has(cacheKey)) {
|
||||
this.states.set(cacheKey, this.buildFakeState(userId, userKeyDefinition, initialValue));
|
||||
}
|
||||
return this.states.get(cacheKey) as FakeSingleUserState<T>;
|
||||
}
|
||||
|
||||
private buildFakeState<T>(
|
||||
userId: UserId,
|
||||
userKeyDefinition: UserKeyDefinition<T>,
|
||||
initialValue?: T,
|
||||
) {
|
||||
const state = new FakeSingleUserState(userId, initialValue, async (...args) => {
|
||||
await this.updateSyncCallback?.(userKeyDefinition, ...args);
|
||||
});
|
||||
state.keyDefinition = userKeyDefinition;
|
||||
return state;
|
||||
}
|
||||
|
||||
private cacheKey(userId: UserId, userKeyDefinition: UserKeyDefinition<unknown>) {
|
||||
return `${userKeyDefinitionCacheKey(userKeyDefinition)}_${userId}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
||||
activeUserId$: Observable<UserId>;
|
||||
states: Map<string, FakeActiveUserState<unknown>> = new Map();
|
||||
|
||||
constructor(
|
||||
public accountService: FakeAccountService,
|
||||
readonly updateSyncCallback?: (
|
||||
key: UserKeyDefinition<unknown>,
|
||||
userId: UserId,
|
||||
newValue: unknown,
|
||||
) => Promise<void>,
|
||||
) {
|
||||
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
|
||||
}
|
||||
|
||||
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
||||
const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
|
||||
let result = this.states.get(cacheKey);
|
||||
|
||||
if (result == null) {
|
||||
result = this.buildFakeState(userKeyDefinition);
|
||||
this.states.set(cacheKey, result);
|
||||
}
|
||||
return result as ActiveUserState<T>;
|
||||
}
|
||||
|
||||
getFake<T>(
|
||||
userKeyDefinition: UserKeyDefinition<T>,
|
||||
{ allowInit }: { allowInit: boolean } = { allowInit: true },
|
||||
): FakeActiveUserState<T> {
|
||||
if (!allowInit && this.states.get(userKeyDefinitionCacheKey(userKeyDefinition)) == null) {
|
||||
return null;
|
||||
}
|
||||
return this.get(userKeyDefinition) as FakeActiveUserState<T>;
|
||||
}
|
||||
|
||||
mockFor<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T): FakeActiveUserState<T> {
|
||||
const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition);
|
||||
if (!this.states.has(cacheKey)) {
|
||||
this.states.set(cacheKey, this.buildFakeState(userKeyDefinition, initialValue));
|
||||
}
|
||||
return this.states.get(cacheKey) as FakeActiveUserState<T>;
|
||||
}
|
||||
|
||||
private buildFakeState<T>(userKeyDefinition: UserKeyDefinition<T>, initialValue?: T) {
|
||||
const state = new FakeActiveUserState<T>(this.accountService, initialValue, async (...args) => {
|
||||
await this.updateSyncCallback?.(userKeyDefinition, ...args);
|
||||
});
|
||||
state.keyDefinition = userKeyDefinition;
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function userKeyDefinitionCacheKey(userKeyDefinition: UserKeyDefinition<unknown>) {
|
||||
return `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`;
|
||||
}
|
||||
|
||||
export class FakeStateProvider implements StateProvider {
|
||||
mock = mock<StateProvider>();
|
||||
getUserState$<T>(userKeyDefinition: UserKeyDefinition<T>, userId?: UserId): Observable<T> {
|
||||
this.mock.getUserState$(userKeyDefinition, userId);
|
||||
if (userId) {
|
||||
return this.getUser<T>(userId, userKeyDefinition).state$;
|
||||
}
|
||||
|
||||
return this.getActive(userKeyDefinition).state$;
|
||||
}
|
||||
|
||||
getUserStateOrDefault$<T>(
|
||||
userKeyDefinition: UserKeyDefinition<T>,
|
||||
config: { userId: UserId | undefined; defaultValue?: T },
|
||||
): Observable<T> {
|
||||
const { userId, defaultValue = null } = config;
|
||||
this.mock.getUserStateOrDefault$(userKeyDefinition, config);
|
||||
if (userId) {
|
||||
return this.getUser<T>(userId, userKeyDefinition).state$;
|
||||
}
|
||||
|
||||
return this.activeUserId$.pipe(
|
||||
take(1),
|
||||
switchMap((userId) =>
|
||||
userId != null ? this.getUser(userId, userKeyDefinition).state$ : of(defaultValue),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async setUserState<T>(
|
||||
userKeyDefinition: UserKeyDefinition<T>,
|
||||
value: T | null,
|
||||
userId?: UserId,
|
||||
): Promise<[UserId, T | null]> {
|
||||
await this.mock.setUserState(userKeyDefinition, value, userId);
|
||||
if (userId) {
|
||||
return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)];
|
||||
} else {
|
||||
return await this.getActive(userKeyDefinition).update(() => value);
|
||||
}
|
||||
}
|
||||
|
||||
getActive<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
||||
return this.activeUser.get(userKeyDefinition);
|
||||
}
|
||||
|
||||
getGlobal<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
||||
return this.global.get(keyDefinition);
|
||||
}
|
||||
|
||||
getUser<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
|
||||
return this.singleUser.get(userId, userKeyDefinition);
|
||||
}
|
||||
|
||||
getDerived<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<unknown, TTo, TDeps>,
|
||||
dependencies: TDeps,
|
||||
): DerivedState<TTo> {
|
||||
return this.derived.get(parentState$, deriveDefinition, dependencies);
|
||||
}
|
||||
|
||||
constructor(public accountService: FakeAccountService) {}
|
||||
|
||||
private distributeSingleUserUpdate(
|
||||
key: UserKeyDefinition<unknown>,
|
||||
userId: UserId,
|
||||
newState: unknown,
|
||||
) {
|
||||
if (this.activeUser.accountService.activeUserId === userId) {
|
||||
const state = this.activeUser.getFake(key, { allowInit: false });
|
||||
state?.nextState(newState, { syncValue: false });
|
||||
}
|
||||
}
|
||||
|
||||
private distributeActiveUserUpdate(
|
||||
key: UserKeyDefinition<unknown>,
|
||||
userId: UserId,
|
||||
newState: unknown,
|
||||
) {
|
||||
this.singleUser
|
||||
.getFake(userId, key, { allowInit: false })
|
||||
?.nextState(newState, { syncValue: false });
|
||||
}
|
||||
|
||||
global: FakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider(
|
||||
this.distributeSingleUserUpdate.bind(this),
|
||||
);
|
||||
activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(
|
||||
this.accountService,
|
||||
this.distributeActiveUserUpdate.bind(this),
|
||||
);
|
||||
derived: FakeDerivedStateProvider = new FakeDerivedStateProvider();
|
||||
activeUserId$: Observable<UserId> = this.activeUser.activeUserId$;
|
||||
}
|
||||
|
||||
export class FakeDerivedStateProvider implements DerivedStateProvider {
|
||||
states: Map<string, DerivedState<unknown>> = new Map();
|
||||
get<TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
dependencies: TDeps,
|
||||
): DerivedState<TTo> {
|
||||
let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState<TTo>;
|
||||
|
||||
if (result == null) {
|
||||
result = new FakeDerivedState(parentState$, deriveDefinition, dependencies);
|
||||
this.states.set(deriveDefinition.buildCacheKey(), result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export {
|
||||
MinimalAccountService,
|
||||
FakeActiveUserAccessor,
|
||||
FakeGlobalStateProvider,
|
||||
FakeSingleUserStateProvider,
|
||||
FakeActiveUserStateProvider,
|
||||
FakeStateProvider,
|
||||
FakeDerivedStateProvider,
|
||||
} from "@bitwarden/state-test-utils";
|
||||
|
||||
@@ -1,279 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable, ReplaySubject, concatMap, filter, firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import {
|
||||
DerivedState,
|
||||
GlobalState,
|
||||
SingleUserState,
|
||||
ActiveUserState,
|
||||
KeyDefinition,
|
||||
DeriveDefinition,
|
||||
UserKeyDefinition,
|
||||
} from "../src/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- using unexposed options for clean typing in test class
|
||||
import { StateUpdateOptions } from "../src/platform/state/state-update-options";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- using unexposed options for clean typing in test class
|
||||
import { CombinedState, activeMarker } from "../src/platform/state/user-state";
|
||||
import { UserId } from "../src/types/guid";
|
||||
import { DerivedStateDependencies } from "../src/types/state";
|
||||
|
||||
import { FakeAccountService } from "./fake-account-service";
|
||||
|
||||
const DEFAULT_TEST_OPTIONS: StateUpdateOptions<any, any> = {
|
||||
shouldUpdate: () => true,
|
||||
combineLatestWith: null,
|
||||
msTimeout: 10,
|
||||
};
|
||||
|
||||
function populateOptionsWithDefault(
|
||||
options: StateUpdateOptions<any, any>,
|
||||
): StateUpdateOptions<any, any> {
|
||||
return {
|
||||
...DEFAULT_TEST_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export class FakeGlobalState<T> implements GlobalState<T> {
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||
stateSubject = new ReplaySubject<T>(1);
|
||||
|
||||
constructor(initialValue?: T) {
|
||||
this.stateSubject.next(initialValue ?? null);
|
||||
}
|
||||
|
||||
nextState(state: T) {
|
||||
this.stateSubject.next(state);
|
||||
}
|
||||
|
||||
async update<TCombine>(
|
||||
configureState: (state: T, dependency: TCombine) => T,
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
): Promise<T> {
|
||||
options = populateOptionsWithDefault(options);
|
||||
if (this.stateSubject["_buffer"].length == 0) {
|
||||
// throw a more helpful not initialized error
|
||||
throw new Error(
|
||||
"You must initialize the state with a value before calling update. Try calling `stateSubject.next(initialState)` before calling update",
|
||||
);
|
||||
}
|
||||
const current = await firstValueFrom(this.state$.pipe(timeout(100)));
|
||||
const combinedDependencies =
|
||||
options.combineLatestWith != null
|
||||
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
||||
: null;
|
||||
if (!options.shouldUpdate(current, combinedDependencies)) {
|
||||
return current;
|
||||
}
|
||||
const newState = configureState(current, combinedDependencies);
|
||||
this.stateSubject.next(newState);
|
||||
this.nextMock(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
/** Tracks update values resolved by `FakeState.update` */
|
||||
nextMock = jest.fn<void, [T]>();
|
||||
|
||||
get state$() {
|
||||
return this.stateSubject.asObservable();
|
||||
}
|
||||
|
||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
||||
get keyDefinition() {
|
||||
if (this._keyDefinition == null) {
|
||||
throw new Error(
|
||||
"Key definition not yet set, usually this means your sut has not asked for this state yet",
|
||||
);
|
||||
}
|
||||
return this._keyDefinition;
|
||||
}
|
||||
set keyDefinition(value: KeyDefinition<T>) {
|
||||
this._keyDefinition = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeSingleUserState<T> implements SingleUserState<T> {
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||
stateSubject = new ReplaySubject<{
|
||||
syncValue: boolean;
|
||||
combinedState: CombinedState<T>;
|
||||
}>(1);
|
||||
|
||||
state$: Observable<T>;
|
||||
combinedState$: Observable<CombinedState<T>>;
|
||||
|
||||
constructor(
|
||||
readonly userId: UserId,
|
||||
initialValue?: T,
|
||||
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
|
||||
) {
|
||||
// Inform the state provider of updates to keep active user states in sync
|
||||
this.stateSubject
|
||||
.pipe(
|
||||
filter((next) => next.syncValue),
|
||||
concatMap(async ({ combinedState }) => {
|
||||
await updateSyncCallback?.(...combinedState);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
this.nextState(initialValue ?? null, { syncValue: initialValue != null });
|
||||
|
||||
this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState));
|
||||
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
||||
}
|
||||
|
||||
nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
|
||||
this.stateSubject.next({
|
||||
syncValue,
|
||||
combinedState: [this.userId, state],
|
||||
});
|
||||
}
|
||||
|
||||
async update<TCombine>(
|
||||
configureState: (state: T | null, dependency: TCombine) => T | null,
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
): Promise<T | null> {
|
||||
options = populateOptionsWithDefault(options);
|
||||
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
||||
const combinedDependencies =
|
||||
options.combineLatestWith != null
|
||||
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
||||
: null;
|
||||
if (!options.shouldUpdate(current, combinedDependencies)) {
|
||||
return current;
|
||||
}
|
||||
const newState = configureState(current, combinedDependencies);
|
||||
this.nextState(newState);
|
||||
this.nextMock(newState);
|
||||
return newState;
|
||||
}
|
||||
|
||||
/** Tracks update values resolved by `FakeState.update` */
|
||||
nextMock = jest.fn<void, [T]>();
|
||||
private _keyDefinition: UserKeyDefinition<T> | null = null;
|
||||
get keyDefinition() {
|
||||
if (this._keyDefinition == null) {
|
||||
throw new Error(
|
||||
"Key definition not yet set, usually this means your sut has not asked for this state yet",
|
||||
);
|
||||
}
|
||||
return this._keyDefinition;
|
||||
}
|
||||
set keyDefinition(value: UserKeyDefinition<T>) {
|
||||
this._keyDefinition = value;
|
||||
}
|
||||
}
|
||||
export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
||||
[activeMarker]: true;
|
||||
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||
stateSubject = new ReplaySubject<{
|
||||
syncValue: boolean;
|
||||
combinedState: CombinedState<T>;
|
||||
}>(1);
|
||||
|
||||
state$: Observable<T>;
|
||||
combinedState$: Observable<CombinedState<T>>;
|
||||
|
||||
constructor(
|
||||
private accountService: FakeAccountService,
|
||||
initialValue?: T,
|
||||
updateSyncCallback?: (userId: UserId, newValue: T) => Promise<void>,
|
||||
) {
|
||||
// Inform the state provider of updates to keep single user states in sync
|
||||
this.stateSubject.pipe(
|
||||
filter((next) => next.syncValue),
|
||||
concatMap(async ({ combinedState }) => {
|
||||
await updateSyncCallback?.(...combinedState);
|
||||
}),
|
||||
);
|
||||
this.nextState(initialValue ?? null, { syncValue: initialValue != null });
|
||||
|
||||
this.combinedState$ = this.stateSubject.pipe(map((v) => v.combinedState));
|
||||
this.state$ = this.combinedState$.pipe(map(([_userId, state]) => state));
|
||||
}
|
||||
|
||||
get userId() {
|
||||
return this.accountService.activeUserId;
|
||||
}
|
||||
|
||||
nextState(state: T | null, { syncValue }: { syncValue: boolean } = { syncValue: true }) {
|
||||
this.stateSubject.next({
|
||||
syncValue,
|
||||
combinedState: [this.userId, state],
|
||||
});
|
||||
}
|
||||
|
||||
async update<TCombine>(
|
||||
configureState: (state: T | null, dependency: TCombine) => T | null,
|
||||
options?: StateUpdateOptions<T, TCombine>,
|
||||
): Promise<[UserId, T | null]> {
|
||||
options = populateOptionsWithDefault(options);
|
||||
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
||||
const combinedDependencies =
|
||||
options.combineLatestWith != null
|
||||
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
||||
: null;
|
||||
if (!options.shouldUpdate(current, combinedDependencies)) {
|
||||
return [this.userId, current];
|
||||
}
|
||||
const newState = configureState(current, combinedDependencies);
|
||||
this.nextState(newState);
|
||||
this.nextMock([this.userId, newState]);
|
||||
return [this.userId, newState];
|
||||
}
|
||||
|
||||
/** Tracks update values resolved by `FakeState.update` */
|
||||
nextMock = jest.fn<void, [[UserId, T]]>();
|
||||
|
||||
private _keyDefinition: UserKeyDefinition<T> | null = null;
|
||||
get keyDefinition() {
|
||||
if (this._keyDefinition == null) {
|
||||
throw new Error(
|
||||
"Key definition not yet set, usually this means your sut has not asked for this state yet",
|
||||
);
|
||||
}
|
||||
return this._keyDefinition;
|
||||
}
|
||||
set keyDefinition(value: UserKeyDefinition<T>) {
|
||||
this._keyDefinition = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class FakeDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>
|
||||
implements DerivedState<TTo>
|
||||
{
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- exposed for testing setup
|
||||
stateSubject = new ReplaySubject<TTo>(1);
|
||||
|
||||
constructor(
|
||||
parentState$: Observable<TFrom>,
|
||||
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
|
||||
dependencies: TDeps,
|
||||
) {
|
||||
parentState$
|
||||
.pipe(
|
||||
concatMap(async (v) => {
|
||||
const newState = deriveDefinition.derive(v, dependencies);
|
||||
if (newState instanceof Promise) {
|
||||
return newState;
|
||||
}
|
||||
return Promise.resolve(newState);
|
||||
}),
|
||||
)
|
||||
.subscribe((newState) => {
|
||||
this.stateSubject.next(newState);
|
||||
});
|
||||
}
|
||||
|
||||
forceValue(value: TTo): Promise<TTo> {
|
||||
this.stateSubject.next(value);
|
||||
return Promise.resolve(value);
|
||||
}
|
||||
forceValueMock = this.forceValue as jest.MockedFunction<typeof this.forceValue>;
|
||||
|
||||
get state$() {
|
||||
return this.stateSubject.asObservable();
|
||||
}
|
||||
}
|
||||
export {
|
||||
FakeGlobalState,
|
||||
FakeSingleUserState,
|
||||
FakeActiveUserState,
|
||||
FakeDerivedState,
|
||||
} from "@bitwarden/state-test-utils";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
|
||||
@@ -78,57 +77,4 @@ export const mockFromSdk = (stub: any) => {
|
||||
return `${stub}_fromSdk`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the emissions of the given observable.
|
||||
*
|
||||
* Call this function before you expect any emissions and then use code that will cause the observable to emit values,
|
||||
* then assert after all expected emissions have occurred.
|
||||
* @param observable
|
||||
* @returns An array that will be populated with all emissions of the observable.
|
||||
*/
|
||||
export function trackEmissions<T>(observable: Observable<T>): T[] {
|
||||
const emissions: T[] = [];
|
||||
observable.subscribe((value) => {
|
||||
switch (value) {
|
||||
case undefined:
|
||||
case null:
|
||||
emissions.push(value);
|
||||
return;
|
||||
default:
|
||||
// process by type
|
||||
break;
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
emissions.push(value);
|
||||
break;
|
||||
case "symbol":
|
||||
// Cheating types to make symbols work at all
|
||||
emissions.push(value.toString() as T);
|
||||
break;
|
||||
default: {
|
||||
emissions.push(clone(value));
|
||||
}
|
||||
}
|
||||
});
|
||||
return emissions;
|
||||
}
|
||||
|
||||
function clone(value: any): any {
|
||||
if (global.structuredClone != undefined) {
|
||||
return structuredClone(value);
|
||||
} else {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
}
|
||||
|
||||
export async function awaitAsync(ms = 1) {
|
||||
if (ms < 1) {
|
||||
await Promise.resolve();
|
||||
} else {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
export { trackEmissions, awaitAsync } from "@bitwarden/core-test-utils";
|
||||
|
||||
@@ -89,8 +89,7 @@ export class DefaultPolicyService implements PolicyService {
|
||||
const policies$ = policies ? of(policies) : this.policies$(userId);
|
||||
return policies$.pipe(
|
||||
map((obsPolicies) => {
|
||||
// TODO: replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) once
|
||||
// FeatureFlag.PM16117_ChangeExistingPasswordRefactor is removed.
|
||||
// TODO ([PM-23777]): replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies))
|
||||
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
|
||||
const filteredPolicies =
|
||||
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { ActiveUserAccessor } from "../../platform/state";
|
||||
import { AccountService } from "../abstractions/account.service";
|
||||
|
||||
/**
|
||||
* Implementation for Platform so they can avoid a direct dependency on AccountService. Not for general consumption.
|
||||
*/
|
||||
export class DefaultActiveUserAccessor implements ActiveUserAccessor {
|
||||
constructor(private readonly accountService: AccountService) {
|
||||
this.activeUserId$ = this.accountService.activeAccount$.pipe(
|
||||
map((a) => (a != null ? a.id : null)),
|
||||
);
|
||||
}
|
||||
|
||||
activeUserId$: Observable<UserId | null>;
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import { of } from "rxjs";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
PinLockType,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
@@ -21,6 +19,8 @@ import {
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "../../../key-management/pin/pin.service.abstraction";
|
||||
import { PinLockType } from "../../../key-management/pin/pin.service.implementation";
|
||||
import { VaultTimeoutSettingsService } from "../../../key-management/vault-timeout";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
|
||||
@@ -14,10 +14,8 @@ import {
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "../../../key-management/pin/pin.service.abstraction";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
@@ -3,7 +3,6 @@ import { PreviewIndividualInvoiceRequest } from "../models/request/preview-indiv
|
||||
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
|
||||
import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax";
|
||||
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
|
||||
import { PreviewTaxAmountResponse } from "../models/response/tax";
|
||||
|
||||
export abstract class TaxServiceAbstraction {
|
||||
abstract getCountries(): CountryListItem[];
|
||||
@@ -20,5 +19,5 @@ export abstract class TaxServiceAbstraction {
|
||||
|
||||
abstract previewTaxAmountForOrganizationTrial: (
|
||||
request: PreviewTaxAmountForOrganizationTrialRequest,
|
||||
) => Promise<PreviewTaxAmountResponse>;
|
||||
) => Promise<number>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax";
|
||||
import { PreviewTaxAmountResponse } from "@bitwarden/common/billing/models/response/tax";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction";
|
||||
@@ -306,13 +305,14 @@ export class TaxService implements TaxServiceAbstraction {
|
||||
|
||||
async previewTaxAmountForOrganizationTrial(
|
||||
request: PreviewTaxAmountForOrganizationTrialRequest,
|
||||
): Promise<PreviewTaxAmountResponse> {
|
||||
return await this.apiService.send(
|
||||
): Promise<number> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/tax/preview-amount/organization-trial",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return response as number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum ClientType {
|
||||
Web = "web",
|
||||
Browser = "browser",
|
||||
Desktop = "desktop",
|
||||
// Mobile = "mobile",
|
||||
Cli = "cli",
|
||||
// DirectoryConnector = "connector",
|
||||
}
|
||||
export { ClientType } from "@bitwarden/client-type";
|
||||
|
||||
@@ -14,8 +14,6 @@ export enum FeatureFlag {
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
|
||||
/* Auth */
|
||||
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
|
||||
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
|
||||
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
|
||||
|
||||
/* Autofill */
|
||||
@@ -34,17 +32,19 @@ export enum FeatureFlag {
|
||||
UseOrganizationWarningsService = "use-organization-warnings-service",
|
||||
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
|
||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
|
||||
/* Key Management */
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
UseSDKForDecryption = "use-sdk-for-decryption",
|
||||
PM17987_BlockType0 = "pm-17987-block-type-0",
|
||||
EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation",
|
||||
ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings",
|
||||
|
||||
/* Tools */
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators",
|
||||
|
||||
/* DIRT */
|
||||
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
||||
|
||||
/* Vault */
|
||||
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
||||
@@ -88,6 +88,10 @@ export const DefaultFeatureFlagValue = {
|
||||
|
||||
/* Tools */
|
||||
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
||||
[FeatureFlag.UseSdkPasswordGenerators]: FALSE,
|
||||
|
||||
/* DIRT */
|
||||
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
||||
|
||||
/* Vault */
|
||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||
@@ -101,8 +105,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
|
||||
|
||||
/* Billing */
|
||||
@@ -113,12 +115,10 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.UseOrganizationWarningsService]: FALSE,
|
||||
[FeatureFlag.AllowTrialLengthZero]: FALSE,
|
||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
[FeatureFlag.UseSDKForDecryption]: FALSE,
|
||||
[FeatureFlag.PM17987_BlockType0]: FALSE,
|
||||
[FeatureFlag.EnrollAeadOnKeyRotation]: FALSE,
|
||||
[FeatureFlag.ForceUpdateKDFSettings]: FALSE,
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class BulkEncryptService {
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
@@ -6,12 +6,20 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
||||
import { CsprngArray } from "../../../types/csprng";
|
||||
|
||||
export abstract class CryptoFunctionService {
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract pbkdf2(
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
algorithm: "sha256" | "sha512",
|
||||
iterations: number,
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract hkdf(
|
||||
ikm: Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
@@ -19,51 +27,76 @@ export abstract class CryptoFunctionService {
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512",
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract hkdfExpand(
|
||||
prk: Uint8Array,
|
||||
info: string | Uint8Array,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512",
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract hash(
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512" | "md5",
|
||||
): Promise<Uint8Array>;
|
||||
abstract hmac(
|
||||
value: Uint8Array,
|
||||
key: Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512",
|
||||
): Promise<Uint8Array>;
|
||||
abstract compare(a: Uint8Array, b: Uint8Array): Promise<boolean>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract hmacFast(
|
||||
value: Uint8Array | string,
|
||||
key: Uint8Array | string,
|
||||
algorithm: "sha1" | "sha256" | "sha512",
|
||||
): Promise<Uint8Array | string>;
|
||||
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract aesDecryptFastParameters(
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey,
|
||||
): CbcDecryptParameters<Uint8Array | string>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract aesDecryptFast({
|
||||
mode,
|
||||
parameters,
|
||||
}:
|
||||
| { mode: "cbc"; parameters: CbcDecryptParameters<Uint8Array | string> }
|
||||
| { mode: "ecb"; parameters: EcbDecryptParameters<Uint8Array | string> }): Promise<string>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Only used by DDG integration until DDG uses PKCS#7 padding, and by lastpass importer.
|
||||
*/
|
||||
abstract aesDecrypt(
|
||||
data: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
key: Uint8Array,
|
||||
mode: "cbc" | "ecb",
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract rsaEncrypt(
|
||||
data: Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
algorithm: "sha1" | "sha256",
|
||||
): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated HAZMAT WARNING: DO NOT USE THIS FOR NEW CODE. Implement low-level crypto operations
|
||||
* in the SDK instead. Further, you should probably never find yourself using this low-level crypto function.
|
||||
*/
|
||||
abstract rsaDecrypt(
|
||||
data: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
@@ -77,7 +110,6 @@ export abstract class CryptoFunctionService {
|
||||
abstract aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise<CsprngArray>;
|
||||
/**
|
||||
* Generates a random array of bytes of the given length. Uses a cryptographically secure random number generator.
|
||||
*
|
||||
* Do not use this for generating encryption keys. Use aesGenerateKey or rsaGenerateKeyPair instead.
|
||||
*/
|
||||
abstract randomBytes(length: number): Promise<CsprngArray>;
|
||||
|
||||
@@ -1,51 +1,8 @@
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "../../../platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { EncString } from "../models/enc-string";
|
||||
|
||||
export abstract class EncryptService {
|
||||
/**
|
||||
* @deprecated
|
||||
* Decrypts an EncString to a string
|
||||
* @param encString - The EncString to decrypt
|
||||
* @param key - The key to decrypt the EncString with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted string
|
||||
*/
|
||||
abstract decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* @deprecated
|
||||
* Decrypts an Encrypted object to a Uint8Array
|
||||
* @param encThing - The Encrypted object to decrypt
|
||||
* @param key - The key to decrypt the Encrypted object with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted Uint8Array
|
||||
*/
|
||||
abstract decryptToBytes(
|
||||
encThing: Encrypted,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<Uint8Array | null>;
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
|
||||
* @param items The items to decrypt
|
||||
* @param key The key to decrypt the items with
|
||||
*/
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
|
||||
/**
|
||||
* Encrypts a string to an EncString
|
||||
* @param plainValue - The value to encrypt
|
||||
@@ -188,12 +145,6 @@ export abstract class EncryptService {
|
||||
decapsulationKey: Uint8Array,
|
||||
): Promise<SymmetricCryptoKey>;
|
||||
|
||||
/**
|
||||
* @deprecated Use @see {@link encapsulateKeyUnsigned} instead
|
||||
* @param data - The data to encrypt
|
||||
* @param publicKey - The public key to encrypt with
|
||||
*/
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
/**
|
||||
* @deprecated Use @see {@link decapsulateKeyUnsigned} instead
|
||||
* @param data - The ciphertext to decrypt
|
||||
@@ -210,6 +161,4 @@ export abstract class EncryptService {
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512",
|
||||
): Promise<string>;
|
||||
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { makeEncString, makeStaticByteArray } from "../../../../spec";
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { EncryptionType } from "../../../platform/enums";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "../../../platform/services/container.service";
|
||||
@@ -83,7 +83,7 @@ describe("EncString", () => {
|
||||
|
||||
const keyService = mock<KeyService>();
|
||||
keyService.hasUserKey.mockResolvedValue(true);
|
||||
keyService.getUserKeyWithLegacySupport.mockResolvedValue(
|
||||
keyService.getUserKey.mockResolvedValue(
|
||||
new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey,
|
||||
);
|
||||
|
||||
@@ -114,67 +114,6 @@ describe("EncString", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptWithKey", () => {
|
||||
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
|
||||
|
||||
const keyService = mock<KeyService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
encryptService.decryptString
|
||||
.calledWith(encString, expect.anything())
|
||||
.mockResolvedValue("decrypted");
|
||||
|
||||
function setupEncryption() {
|
||||
encryptService.encryptString.mockImplementation(async (data, key) => {
|
||||
return makeEncString(data);
|
||||
});
|
||||
encryptService.decryptString.mockImplementation(async (encString, key) => {
|
||||
return encString.data;
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||
});
|
||||
|
||||
it("decrypts using the provided key and encryptService", async () => {
|
||||
setupEncryption();
|
||||
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
|
||||
await encString.decryptWithKey(key, encryptService);
|
||||
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
|
||||
it("fails to decrypt when key is null", async () => {
|
||||
const decrypted = await encString.decryptWithKey(null, encryptService);
|
||||
|
||||
expect(decrypted).toBe("[error: cannot decrypt]");
|
||||
expect(encString.decryptedValue).toBe("[error: cannot decrypt]");
|
||||
});
|
||||
|
||||
it("fails to decrypt when encryptService is null", async () => {
|
||||
const decrypted = await encString.decryptWithKey(
|
||||
new SymmetricCryptoKey(makeStaticByteArray(32)),
|
||||
null,
|
||||
);
|
||||
|
||||
expect(decrypted).toBe("[error: cannot decrypt]");
|
||||
expect(encString.decryptedValue).toBe("[error: cannot decrypt]");
|
||||
});
|
||||
|
||||
it("fails to decrypt when encryptService throws", async () => {
|
||||
encryptService.decryptString.mockRejectedValue("error");
|
||||
|
||||
const decrypted = await encString.decryptWithKey(
|
||||
new SymmetricCryptoKey(makeStaticByteArray(32)),
|
||||
encryptService,
|
||||
);
|
||||
|
||||
expect(decrypted).toBe("[error: cannot decrypt]");
|
||||
expect(encString.decryptedValue).toBe("[error: cannot decrypt]");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AesCbc256_B64", () => {
|
||||
it("constructor", () => {
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
|
||||
@@ -343,7 +282,7 @@ describe("EncString", () => {
|
||||
|
||||
await encString.decrypt(null, key);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
|
||||
expect(keyService.getUserKey).not.toHaveBeenCalled();
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, key);
|
||||
});
|
||||
|
||||
@@ -361,11 +300,11 @@ describe("EncString", () => {
|
||||
it("gets the user's decryption key if required", async () => {
|
||||
const userKey = mock<UserKey>();
|
||||
|
||||
keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
|
||||
keyService.getUserKey.mockResolvedValue(userKey);
|
||||
|
||||
await encString.decrypt(null, null);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
|
||||
expect(keyService.getUserKey).toHaveBeenCalledWith();
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, userKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { EncString as SdkEncString } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../platform/enums";
|
||||
import { Encrypted } from "../../../platform/interfaces/encrypted";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export const DECRYPT_ERROR = "[error: cannot decrypt]";
|
||||
|
||||
export class EncString implements Encrypted {
|
||||
encryptedString?: EncryptedString;
|
||||
encryptedString?: SdkEncString;
|
||||
encryptionType?: EncryptionType;
|
||||
decryptedValue?: string;
|
||||
data?: string;
|
||||
@@ -43,7 +44,11 @@ export class EncString implements Encrypted {
|
||||
return this.data == null ? null : Utils.fromB64ToArray(this.data);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
toSdk(): SdkEncString {
|
||||
return this.encryptedString;
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return this.encryptedString as string;
|
||||
}
|
||||
|
||||
@@ -57,14 +62,14 @@ export class EncString implements Encrypted {
|
||||
|
||||
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
|
||||
if (iv != null) {
|
||||
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
|
||||
this.encryptedString = (encType + "." + iv + "|" + data) as SdkEncString;
|
||||
} else {
|
||||
this.encryptedString = (encType + "." + data) as EncryptedString;
|
||||
this.encryptedString = (encType + "." + data) as SdkEncString;
|
||||
}
|
||||
|
||||
// mac
|
||||
if (mac != null) {
|
||||
this.encryptedString = (this.encryptedString + "|" + mac) as EncryptedString;
|
||||
this.encryptedString = (this.encryptedString + "|" + mac) as SdkEncString;
|
||||
}
|
||||
|
||||
this.encryptionType = encType;
|
||||
@@ -74,7 +79,7 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
|
||||
private initFromEncryptedString(encryptedString: string) {
|
||||
this.encryptedString = encryptedString as EncryptedString;
|
||||
this.encryptedString = encryptedString as SdkEncString;
|
||||
if (!this.encryptedString) {
|
||||
return;
|
||||
}
|
||||
@@ -184,31 +189,14 @@ export class EncString implements Encrypted {
|
||||
return this.decryptedValue;
|
||||
}
|
||||
|
||||
async decryptWithKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encryptService: EncryptService,
|
||||
decryptTrace: string = "domain-withkey",
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (key == null) {
|
||||
throw new Error("No key to decrypt EncString");
|
||||
}
|
||||
|
||||
this.decryptedValue = await encryptService.decryptString(this, key);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
this.decryptedValue = DECRYPT_ERROR;
|
||||
}
|
||||
|
||||
return this.decryptedValue;
|
||||
}
|
||||
private async getKeyForDecryption(orgId: string) {
|
||||
const keyService = Utils.getContainerService().getKeyService();
|
||||
return orgId != null
|
||||
? await keyService.getOrgKey(orgId)
|
||||
: await keyService.getUserKeyWithLegacySupport();
|
||||
return orgId != null ? await keyService.getOrgKey(orgId) : await keyService.getUserKey();
|
||||
}
|
||||
}
|
||||
|
||||
export type EncryptedString = Opaque<string, "EncString">;
|
||||
/**
|
||||
* Temporary type mapping until consumers are moved over.
|
||||
* @deprecated - Use SdkEncString directly
|
||||
*/
|
||||
export type EncryptedString = SdkEncString;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
|
||||
/**
|
||||
* @deprecated Will be deleted in an immediate subsequent PR
|
||||
*/
|
||||
export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
|
||||
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected logService: LogService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Decrypts items using a web worker if the environment supports it.
|
||||
* Will fall back to the main thread if the window object is not available.
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
if (items == null || items.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
results.push(await items[i].decrypt(key));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {}
|
||||
}
|
||||
@@ -5,15 +5,11 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-st
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export class EncryptServiceImplementation implements EncryptService {
|
||||
@@ -23,7 +19,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
protected logMacFailures: boolean,
|
||||
) {}
|
||||
|
||||
// Proxy functions; Their implementation are temporary before moving at this level to the SDK
|
||||
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (plainValue == null) {
|
||||
this.logService.warning(
|
||||
@@ -171,36 +166,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return Utils.fromBufferToB64(hashArray);
|
||||
}
|
||||
|
||||
// Handle updating private properties to turn on/off feature flags.
|
||||
onServerConfigChange(newConfig: ServerConfig): void {}
|
||||
|
||||
async decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
_decryptContext: string = "no context",
|
||||
): Promise<string> {
|
||||
await SdkLoadService.Ready;
|
||||
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.toEncoded());
|
||||
}
|
||||
|
||||
async decryptToBytes(
|
||||
encThing: Encrypted,
|
||||
key: SymmetricCryptoKey,
|
||||
_decryptContext: string = "no context",
|
||||
): Promise<Uint8Array | null> {
|
||||
if (encThing.encryptionType == null || encThing.ivBytes == null || encThing.dataBytes == null) {
|
||||
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
|
||||
}
|
||||
const buffer = EncArrayBuffer.fromParts(
|
||||
encThing.encryptionType,
|
||||
encThing.ivBytes,
|
||||
encThing.dataBytes,
|
||||
encThing.macBytes,
|
||||
).buffer;
|
||||
await SdkLoadService.Ready;
|
||||
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.toEncoded());
|
||||
}
|
||||
|
||||
async encapsulateKeyUnsigned(
|
||||
sharedKey: SymmetricCryptoKey,
|
||||
encapsulationKey: Uint8Array,
|
||||
@@ -228,45 +193,14 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No decapsulationKey provided for decapsulation");
|
||||
}
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
const keyBytes = PureCrypto.decapsulate_key_unsigned(
|
||||
encryptedSharedKey.encryptedString,
|
||||
decapsulationKey,
|
||||
);
|
||||
await SdkLoadService.Ready;
|
||||
return new SymmetricCryptoKey(keyBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService (PM-4154)
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
if (items == null || items.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// don't use promise.all because this task is not io bound
|
||||
const results = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
results.push(await items[i].decrypt(key));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
if (data == null) {
|
||||
throw new Error("No data provided for encryption.");
|
||||
}
|
||||
|
||||
if (publicKey == null) {
|
||||
throw new Error("No public key provided for encryption.");
|
||||
}
|
||||
const encrypted = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encrypted));
|
||||
}
|
||||
|
||||
async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
if (data == null) {
|
||||
throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption.");
|
||||
|
||||
@@ -303,12 +303,6 @@ describe("EncryptService", () => {
|
||||
const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey);
|
||||
expect(actual).toEqual(new EncString("encapsulated_key_unsigned"));
|
||||
});
|
||||
|
||||
it("throws if no data was provided", () => {
|
||||
return expect(encryptService.rsaEncrypt(null, new Uint8Array(32))).rejects.toThrow(
|
||||
"No data provided for encryption",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decapsulateKeyUnsigned", () => {
|
||||
@@ -338,23 +332,4 @@ describe("EncryptService", () => {
|
||||
expect(cryptoFunctionService.hash).toHaveBeenCalledWith("test", "sha256");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptItems", () => {
|
||||
it("returns empty array if no items are provided", async () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const actual = await encryptService.decryptItems(null, key);
|
||||
expect(actual).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns items decrypted with provided key", async () => {
|
||||
const key = mock<SymmetricCryptoKey>();
|
||||
const decryptable = {
|
||||
decrypt: jest.fn().mockResolvedValue("decrypted"),
|
||||
};
|
||||
const items = [decryptable];
|
||||
const actual = await encryptService.decryptItems(items as any, key);
|
||||
expect(actual).toEqual(["decrypted"]);
|
||||
expect(decryptable.decrypt).toHaveBeenCalledWith(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "../../../platform/services/console-log.service";
|
||||
import { ContainerService } from "../../../platform/services/container.service";
|
||||
import { getClassInitializer } from "../../../platform/services/cryptography/get-class-initializer";
|
||||
import {
|
||||
DECRYPT_COMMAND,
|
||||
SET_CONFIG_COMMAND,
|
||||
ParsedDecryptCommandData,
|
||||
} from "../types/worker-command.type";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
import { WebCryptoFunctionService } from "./web-crypto-function.service";
|
||||
|
||||
const workerApi: Worker = self as any;
|
||||
|
||||
let inited = false;
|
||||
let encryptService: EncryptServiceImplementation;
|
||||
let logService: LogService;
|
||||
|
||||
/**
|
||||
* Bootstrap the worker environment with services required for decryption
|
||||
*/
|
||||
export function init() {
|
||||
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
logService = new ConsoleLogService(false);
|
||||
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
|
||||
const bitwardenContainerService = new ContainerService(null, encryptService);
|
||||
bitwardenContainerService.attachToGlobal(self);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for messages and decrypt their contents
|
||||
*/
|
||||
workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
if (!inited) {
|
||||
init();
|
||||
}
|
||||
|
||||
const request: {
|
||||
command: string;
|
||||
} = JSON.parse(event.data);
|
||||
|
||||
switch (request.command) {
|
||||
case DECRYPT_COMMAND:
|
||||
return await handleDecrypt(request as unknown as ParsedDecryptCommandData);
|
||||
case SET_CONFIG_COMMAND: {
|
||||
const newConfig = (request as unknown as { newConfig: Jsonify<ServerConfig> }).newConfig;
|
||||
return await handleSetConfig(newConfig);
|
||||
}
|
||||
default:
|
||||
logService.error(`[EncryptWorker] unknown worker command`, request.command, request);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleDecrypt(request: ParsedDecryptCommandData) {
|
||||
const key = SymmetricCryptoKey.fromJSON(request.key);
|
||||
const items = request.items.map((jsonItem) => {
|
||||
const initializer = getClassInitializer<Decryptable<any>>(jsonItem.initializerKey);
|
||||
return initializer(jsonItem);
|
||||
});
|
||||
const result = await encryptService.decryptItems(items, key);
|
||||
|
||||
workerApi.postMessage({
|
||||
id: request.id,
|
||||
items: JSON.stringify(result),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSetConfig(newConfig: Jsonify<ServerConfig>) {
|
||||
encryptService.onServerConfigChange(ServerConfig.fromJSON(newConfig));
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
/**
|
||||
* @deprecated Will be deleted in an immediate subsequent PR
|
||||
*/
|
||||
export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||
private featureFlagEncryptService: BulkEncryptService;
|
||||
private currentServerConfig: ServerConfig | undefined = undefined;
|
||||
|
||||
constructor(protected encryptService: EncryptService) {}
|
||||
|
||||
/**
|
||||
* Decrypts items using a web worker if the environment supports it.
|
||||
* Will fall back to the main thread if the window object is not available.
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
return await this.encryptService.decryptItems(items, key);
|
||||
}
|
||||
|
||||
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
|
||||
/**
|
||||
* @deprecated Will be deleted in an immediate subsequent PR
|
||||
*/
|
||||
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
|
||||
protected useSDKForDecryption: boolean = true;
|
||||
|
||||
/**
|
||||
* Sends items to a web worker to decrypt them.
|
||||
* This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI).
|
||||
*/
|
||||
async decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]> {
|
||||
return await super.decryptItems(items, key);
|
||||
}
|
||||
|
||||
override onServerConfigChange(newConfig: ServerConfig): void {}
|
||||
}
|
||||
@@ -154,46 +154,6 @@ describe("WebCrypto Function Service", () => {
|
||||
testHmac("sha512", Sha512Mac);
|
||||
});
|
||||
|
||||
describe("compare", () => {
|
||||
it("should successfully compare two of the same values", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const equal = await cryptoFunctionService.compare(a, a);
|
||||
expect(equal).toBe(true);
|
||||
});
|
||||
|
||||
it("should successfully compare two different values of the same length", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
b[1] = 4;
|
||||
const equal = await cryptoFunctionService.compare(a, b);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
|
||||
it("should successfully compare two different values of different lengths", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
const equal = await cryptoFunctionService.compare(a, b);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hmacFast", () => {
|
||||
testHmacFast("sha1", Sha1Mac);
|
||||
testHmacFast("sha256", Sha256Mac);
|
||||
testHmacFast("sha512", Sha512Mac);
|
||||
});
|
||||
|
||||
describe("compareFast", () => {
|
||||
it("should successfully compare two of the same values", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
@@ -523,20 +483,6 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function testHmacFast(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
|
||||
it("should create valid " + algorithm + " hmac", async () => {
|
||||
const cryptoFunctionService = getWebCryptoFunctionService();
|
||||
const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("secretkey"));
|
||||
const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("SignMe!!"));
|
||||
const computedMac = await cryptoFunctionService.hmacFast(
|
||||
dataByteString,
|
||||
keyByteString,
|
||||
algorithm,
|
||||
);
|
||||
expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac))).toBe(mac);
|
||||
});
|
||||
}
|
||||
|
||||
function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) {
|
||||
it(
|
||||
"should successfully generate a " + length + " bit key pair",
|
||||
|
||||
@@ -146,34 +146,6 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
async compare(a: Uint8Array, b: Uint8Array): Promise<boolean> {
|
||||
const macKey = await this.randomBytes(32);
|
||||
const signingAlgorithm = {
|
||||
name: "HMAC",
|
||||
hash: { name: "SHA-256" },
|
||||
};
|
||||
const impKey = await this.subtle.importKey("raw", macKey, signingAlgorithm, false, ["sign"]);
|
||||
const mac1 = await this.subtle.sign(signingAlgorithm, impKey, a);
|
||||
const mac2 = await this.subtle.sign(signingAlgorithm, impKey, b);
|
||||
|
||||
if (mac1.byteLength !== mac2.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arr1 = new Uint8Array(mac1);
|
||||
const arr2 = new Uint8Array(mac2);
|
||||
for (let i = 0; i < arr2.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
hmacFast(value: string, key: string, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
|
||||
const hmac = forge.hmac.create();
|
||||
hmac.start(algorithm, key);
|
||||
@@ -182,6 +154,9 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
|
||||
return Promise.resolve(bytes);
|
||||
}
|
||||
|
||||
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
async compareFast(a: string, b: string): Promise<boolean> {
|
||||
const rand = await this.randomBytes(32);
|
||||
const bytes = new Uint32Array(rand);
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import {
|
||||
DECRYPT_COMMAND,
|
||||
DecryptCommandData,
|
||||
SET_CONFIG_COMMAND,
|
||||
buildDecryptMessage,
|
||||
buildSetConfigMessage,
|
||||
} from "./worker-command.type";
|
||||
|
||||
describe("Worker command types", () => {
|
||||
describe("buildDecryptMessage", () => {
|
||||
it("builds a message with the correct command", () => {
|
||||
const commandData = createDecryptCommandData();
|
||||
|
||||
const result = buildDecryptMessage(commandData);
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(DECRYPT_COMMAND);
|
||||
});
|
||||
|
||||
it("includes the provided data in the message", () => {
|
||||
const mockItems = [{ encrypted: "test-encrypted" } as unknown as Decryptable<any>];
|
||||
const commandData = createDecryptCommandData(mockItems);
|
||||
|
||||
const result = buildDecryptMessage(commandData);
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(DECRYPT_COMMAND);
|
||||
expect(parsedResult.id).toBe("test-id");
|
||||
expect(parsedResult.items).toEqual(mockItems);
|
||||
expect(SymmetricCryptoKey.fromJSON(parsedResult.key)).toEqual(commandData.key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSetConfigMessage", () => {
|
||||
it("builds a message with the correct command", () => {
|
||||
const result = buildSetConfigMessage({ newConfig: mock<ServerConfig>() });
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(SET_CONFIG_COMMAND);
|
||||
});
|
||||
|
||||
it("includes the provided data in the message", () => {
|
||||
const serverConfig = { version: "test-version" } as unknown as ServerConfig;
|
||||
|
||||
const result = buildSetConfigMessage({ newConfig: serverConfig });
|
||||
|
||||
const parsedResult = JSON.parse(result);
|
||||
expect(parsedResult.command).toBe(SET_CONFIG_COMMAND);
|
||||
expect(ServerConfig.fromJSON(parsedResult.newConfig).version).toEqual(serverConfig.version);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createDecryptCommandData(items?: Decryptable<any>[]): DecryptCommandData {
|
||||
return {
|
||||
id: "test-id",
|
||||
items: items ?? [],
|
||||
key: new SymmetricCryptoKey(makeStaticByteArray(64)),
|
||||
};
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export const DECRYPT_COMMAND = "decrypt";
|
||||
export const SET_CONFIG_COMMAND = "updateConfig";
|
||||
|
||||
export type DecryptCommandData = {
|
||||
id: string;
|
||||
items: Decryptable<any>[];
|
||||
key: SymmetricCryptoKey;
|
||||
};
|
||||
|
||||
export type ParsedDecryptCommandData = {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
};
|
||||
|
||||
type SetConfigCommandData = { newConfig: ServerConfig };
|
||||
|
||||
export function buildDecryptMessage(data: DecryptCommandData): string {
|
||||
return JSON.stringify({
|
||||
command: DECRYPT_COMMAND,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSetConfigMessage(data: SetConfigCommandData): string {
|
||||
return JSON.stringify({
|
||||
command: SET_CONFIG_COMMAND,
|
||||
...data,
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import {
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
export abstract class MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
@@ -12,14 +20,23 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract forceSetPasswordReason$: (userId: UserId) => Observable<ForceSetPasswordReason>;
|
||||
/**
|
||||
* An observable that emits the master password salt for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
* @throws If the user ID is provided, but the user is not found.
|
||||
*/
|
||||
abstract saltForUser$: (userId: UserId) => Observable<MasterPasswordSalt>;
|
||||
/**
|
||||
* An observable that emits the master key for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordUnlockData}, {@link makeMasterPasswordAuthenticationData} or {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract masterKey$: (userId: UserId) => Observable<MasterKey>;
|
||||
/**
|
||||
* An observable that emits the master key hash for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link makeMasterPasswordAuthenticationData}.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
@@ -32,6 +49,7 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
|
||||
/**
|
||||
* Decrypts the user key with the provided master key
|
||||
* @deprecated Interacting with the master-key directly is deprecated. Please use {@link unwrapUserKeyFromMasterPasswordUnlockData} instead.
|
||||
* @param masterKey The user's master key
|
||||
* * @param userId The desired user
|
||||
* @param userKey The user's encrypted symmetric key
|
||||
@@ -44,12 +62,52 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||
userId: string,
|
||||
userKey?: EncString,
|
||||
) => Promise<UserKey | null>;
|
||||
|
||||
/**
|
||||
* Makes the authentication hash for authenticating to the server with the master password.
|
||||
* @param password The master password.
|
||||
* @param kdf The KDF configuration.
|
||||
* @param salt The master password salt to use. See {@link saltForUser$} for current salt.
|
||||
* @throws If password, KDF or salt are null or undefined.
|
||||
*/
|
||||
abstract makeMasterPasswordAuthenticationData: (
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
) => Promise<MasterPasswordAuthenticationData>;
|
||||
|
||||
/**
|
||||
* Creates a MasterPasswordUnlockData bundle that encrypts the user-key with a key derived from the password. The
|
||||
* bundle also contains the KDF settings and salt used to derive the key, which are required to decrypt the user-key later.
|
||||
* @param password The master password.
|
||||
* @param kdf The KDF configuration.
|
||||
* @param salt The master password salt to use. See {@link saltForUser$} for current salt.
|
||||
* @param userKey The user's userKey to encrypt.
|
||||
* @throws If password, KDF, salt, or userKey are null or undefined.
|
||||
*/
|
||||
abstract makeMasterPasswordUnlockData: (
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
userKey: UserKey,
|
||||
) => Promise<MasterPasswordUnlockData>;
|
||||
|
||||
/**
|
||||
* Unwraps a user-key that was wrapped with a password provided KDF settings. The same KDF settings and salt must be provided to unwrap the user-key, otherwise it will fail to decrypt.
|
||||
* @throws If the encryption type is not supported.
|
||||
* @throws If the password, KDF, or salt don't match the original wrapping parameters.
|
||||
*/
|
||||
abstract unwrapUserKeyFromMasterPasswordUnlockData: (
|
||||
password: string,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
) => Promise<UserKey>;
|
||||
}
|
||||
|
||||
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
* Set the master key for the user.
|
||||
* Note: Use {@link clearMasterKey} to clear the master key.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param masterKey The master key.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or master key is missing.
|
||||
@@ -57,6 +115,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear the master key for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
@@ -64,6 +123,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
/**
|
||||
* Set the master key hash for the user.
|
||||
* Note: Use {@link clearMasterKeyHash} to clear the master key hash.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param masterKeyHash The master key hash.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or master key hash is missing.
|
||||
@@ -71,6 +131,7 @@ export abstract class InternalMasterPasswordServiceAbstraction extends MasterPas
|
||||
abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear the master key hash for the user.
|
||||
* @deprecated Interacting with the master-key directly is deprecated.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
|
||||
@@ -3,11 +3,20 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { ReplaySubject, Observable } from "rxjs";
|
||||
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||
mock = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
@@ -24,6 +33,10 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
this.masterKeyHashSubject.next(initialMasterKeyHash);
|
||||
}
|
||||
|
||||
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
||||
return this.mock.saltForUser$(userId);
|
||||
}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
return this.masterKeySubject.asObservable();
|
||||
}
|
||||
@@ -71,4 +84,28 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||
): Promise<UserKey> {
|
||||
return this.mock.decryptUserKeyWithMasterKey(masterKey, userId, userKey);
|
||||
}
|
||||
|
||||
makeMasterPasswordAuthenticationData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
): Promise<MasterPasswordAuthenticationData> {
|
||||
return this.mock.makeMasterPasswordAuthenticationData(password, kdf, salt);
|
||||
}
|
||||
|
||||
makeMasterPasswordUnlockData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
userKey: UserKey,
|
||||
): Promise<MasterPasswordUnlockData> {
|
||||
return this.mock.makeMasterPasswordUnlockData(password, kdf, salt, userKey);
|
||||
}
|
||||
|
||||
unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
password: string,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
): Promise<UserKey> {
|
||||
return this.mock.unwrapUserKeyFromMasterPasswordUnlockData(password, masterPasswordUnlockData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
import * as rxjs from "rxjs";
|
||||
|
||||
import { makeSymmetricCryptoKey } from "../../../../spec";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
FakeAccountService,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../spec";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
@@ -10,9 +19,11 @@ import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
import { MasterPasswordSalt } from "../types/master-password.types";
|
||||
|
||||
import { MasterPasswordService } from "./master-password.service";
|
||||
|
||||
@@ -24,8 +35,10 @@ describe("MasterPasswordService", () => {
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let cryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
const userId = "user-id" as UserId;
|
||||
const userId = "00000000-0000-0000-0000-000000000000" as UserId;
|
||||
const mockUserState = {
|
||||
state$: of(null),
|
||||
update: jest.fn().mockResolvedValue(null),
|
||||
@@ -45,6 +58,8 @@ describe("MasterPasswordService", () => {
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
logService = mock<LogService>();
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
accountService = mockAccountServiceWith(userId);
|
||||
|
||||
stateProvider.getUser.mockReturnValue(mockUserState as any);
|
||||
|
||||
@@ -56,10 +71,33 @@ describe("MasterPasswordService", () => {
|
||||
keyGenerationService,
|
||||
encryptService,
|
||||
logService,
|
||||
cryptoFunctionService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
encryptService.unwrapSymmetricKey.mockResolvedValue(makeSymmetricCryptoKey(64, 1));
|
||||
keyGenerationService.stretchKey.mockResolvedValue(makeSymmetricCryptoKey(64, 3));
|
||||
Object.defineProperty(SdkLoadService, "Ready", {
|
||||
value: Promise.resolve(),
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("saltForUser$", () => {
|
||||
it("throws when userid not present", async () => {
|
||||
expect(() => {
|
||||
sut.saltForUser$(null as unknown as UserId);
|
||||
}).toThrow("userId is null or undefined.");
|
||||
});
|
||||
it("throws when userid present but not in account service", async () => {
|
||||
await expect(
|
||||
firstValueFrom(sut.saltForUser$("00000000-0000-0000-0000-000000000001" as UserId)),
|
||||
).rejects.toThrow("Cannot read properties of undefined (reading 'email')");
|
||||
});
|
||||
it("returns salt", async () => {
|
||||
const salt = await firstValueFrom(sut.saltForUser$(userId));
|
||||
expect(salt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setForceSetPasswordReason", () => {
|
||||
@@ -190,4 +228,97 @@ describe("MasterPasswordService", () => {
|
||||
expect(updateFn(null)).toEqual(encryptedKey.toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeMasterPasswordAuthenticationData", () => {
|
||||
const password = "test-password";
|
||||
const kdf: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||
const masterKey = makeSymmetricCryptoKey(32, 2);
|
||||
const masterKeyHash = makeSymmetricCryptoKey(32, 3).toEncoded();
|
||||
|
||||
beforeEach(() => {
|
||||
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(masterKey);
|
||||
cryptoFunctionService.pbkdf2.mockResolvedValue(masterKeyHash);
|
||||
});
|
||||
|
||||
it("derives master key and creates authentication hash", async () => {
|
||||
const result = await sut.makeMasterPasswordAuthenticationData(password, kdf, salt);
|
||||
|
||||
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(password, salt, kdf);
|
||||
expect(cryptoFunctionService.pbkdf2).toHaveBeenCalledWith(
|
||||
masterKey.toEncoded(),
|
||||
password,
|
||||
"sha256",
|
||||
1,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
kdf,
|
||||
salt,
|
||||
masterPasswordAuthenticationHash: Utils.fromBufferToB64(masterKeyHash),
|
||||
});
|
||||
});
|
||||
|
||||
it("throws if password is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordAuthenticationData(null as unknown as string, kdf, salt),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if kdf is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordAuthenticationData(password, null as unknown as KdfConfig, salt),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if salt is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordAuthenticationData(
|
||||
password,
|
||||
kdf,
|
||||
null as unknown as MasterPasswordSalt,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("wrapUnwrapUserKeyWithPassword", () => {
|
||||
const password = "test-password";
|
||||
const kdf: KdfConfig = new PBKDF2KdfConfig(600_000);
|
||||
const salt = "test@bitwarden.com" as MasterPasswordSalt;
|
||||
const userKey = makeSymmetricCryptoKey(64, 2) as UserKey;
|
||||
|
||||
it("wraps and unwraps user key with password", async () => {
|
||||
const unlockData = await sut.makeMasterPasswordUnlockData(password, kdf, salt, userKey);
|
||||
const unwrappedUserkey = await sut.unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
password,
|
||||
unlockData,
|
||||
);
|
||||
expect(unwrappedUserkey).toEqual(userKey);
|
||||
});
|
||||
|
||||
it("throws if password is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(null as unknown as string, kdf, salt, userKey),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if kdf is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(password, null as unknown as KdfConfig, salt, userKey),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if salt is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(
|
||||
password,
|
||||
kdf,
|
||||
null as unknown as MasterPasswordSalt,
|
||||
userKey,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
it("throws if userKey is null", async () => {
|
||||
await expect(
|
||||
sut.makeMasterPasswordUnlockData(password, kdf, salt, null as unknown as UserKey),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
@@ -16,9 +24,17 @@ import {
|
||||
} from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { CryptoFunctionService } from "../../crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { EncryptedString, EncString } from "../../crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
MasterKeyWrappedUserKey,
|
||||
MasterPasswordAuthenticationData,
|
||||
MasterPasswordAuthenticationHash,
|
||||
MasterPasswordSalt,
|
||||
MasterPasswordUnlockData,
|
||||
} from "../types/master-password.types";
|
||||
|
||||
/** Memory since master key shouldn't be available on lock */
|
||||
const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMORY, "masterKey", {
|
||||
@@ -59,8 +75,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private encryptService: EncryptService,
|
||||
private logService: LogService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
saltForUser$(userId: UserId): Observable<MasterPasswordSalt> {
|
||||
assertNonNullish(userId, "userId");
|
||||
return this.accountService.accounts$.pipe(
|
||||
map((accounts) => accounts[userId].email),
|
||||
map((email) => this.emailToSalt(email)),
|
||||
);
|
||||
}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
@@ -95,6 +121,10 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
return EncString.fromJSON(key);
|
||||
}
|
||||
|
||||
private emailToSalt(email: string): MasterPasswordSalt {
|
||||
return email.toLowerCase().trim() as MasterPasswordSalt;
|
||||
}
|
||||
|
||||
async setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> {
|
||||
if (masterKey == null) {
|
||||
throw new Error("Master key is required.");
|
||||
@@ -202,4 +232,89 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
|
||||
return decUserKey as UserKey;
|
||||
}
|
||||
|
||||
async makeMasterPasswordAuthenticationData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
): Promise<MasterPasswordAuthenticationData> {
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(kdf, "kdf");
|
||||
assertNonNullish(salt, "salt");
|
||||
|
||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||
|
||||
const SERVER_AUTHENTICATION_HASH_ITERATIONS = 1;
|
||||
|
||||
const masterKey = (await this.keyGenerationService.deriveKeyFromPassword(
|
||||
password,
|
||||
salt,
|
||||
kdf,
|
||||
)) as MasterKey;
|
||||
|
||||
const masterPasswordAuthenticationHash = Utils.fromBufferToB64(
|
||||
await this.cryptoFunctionService.pbkdf2(
|
||||
masterKey.toEncoded(),
|
||||
password,
|
||||
"sha256",
|
||||
SERVER_AUTHENTICATION_HASH_ITERATIONS,
|
||||
),
|
||||
) as MasterPasswordAuthenticationHash;
|
||||
|
||||
return {
|
||||
salt,
|
||||
kdf,
|
||||
masterPasswordAuthenticationHash,
|
||||
} as MasterPasswordAuthenticationData;
|
||||
}
|
||||
|
||||
async makeMasterPasswordUnlockData(
|
||||
password: string,
|
||||
kdf: KdfConfig,
|
||||
salt: MasterPasswordSalt,
|
||||
userKey: UserKey,
|
||||
): Promise<MasterPasswordUnlockData> {
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(kdf, "kdf");
|
||||
assertNonNullish(salt, "salt");
|
||||
assertNonNullish(userKey, "userKey");
|
||||
|
||||
// We don't trust callers to use masterpasswordsalt correctly. They may type assert incorrectly.
|
||||
salt = salt.toLowerCase().trim() as MasterPasswordSalt;
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
const masterKeyWrappedUserKey = new EncString(
|
||||
PureCrypto.encrypt_user_key_with_master_password(
|
||||
userKey.toEncoded(),
|
||||
password,
|
||||
salt,
|
||||
kdf.toSdkConfig(),
|
||||
),
|
||||
) as MasterKeyWrappedUserKey;
|
||||
return {
|
||||
salt,
|
||||
kdf,
|
||||
masterKeyWrappedUserKey,
|
||||
};
|
||||
}
|
||||
|
||||
async unwrapUserKeyFromMasterPasswordUnlockData(
|
||||
password: string,
|
||||
masterPasswordUnlockData: MasterPasswordUnlockData,
|
||||
): Promise<UserKey> {
|
||||
assertNonNullish(password, "password");
|
||||
assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData");
|
||||
|
||||
await SdkLoadService.Ready;
|
||||
const userKey = new SymmetricCryptoKey(
|
||||
PureCrypto.decrypt_user_key_with_master_password(
|
||||
masterPasswordUnlockData.masterKeyWrappedUserKey.encryptedString,
|
||||
password,
|
||||
masterPasswordUnlockData.salt,
|
||||
masterPasswordUnlockData.kdf.toSdkConfig(),
|
||||
),
|
||||
);
|
||||
return userKey as UserKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { EncString } from "../../crypto/models/enc-string";
|
||||
|
||||
/**
|
||||
* The Base64-encoded master password authentication hash, that is sent to the server for authentication.
|
||||
*/
|
||||
export type MasterPasswordAuthenticationHash = Opaque<string, "MasterPasswordAuthenticationHash">;
|
||||
/**
|
||||
* You MUST obtain this through the emailToSalt function in MasterPasswordService
|
||||
*/
|
||||
export type MasterPasswordSalt = Opaque<string, "MasterPasswordSalt">;
|
||||
export type MasterKeyWrappedUserKey = Opaque<EncString, "MasterPasswordSalt">;
|
||||
|
||||
/**
|
||||
* The data required to unlock with the master password.
|
||||
*/
|
||||
export type MasterPasswordUnlockData = {
|
||||
salt: MasterPasswordSalt;
|
||||
kdf: KdfConfig;
|
||||
masterKeyWrappedUserKey: MasterKeyWrappedUserKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* The data required to authenticate with the master password.
|
||||
*/
|
||||
export type MasterPasswordAuthenticationData = {
|
||||
salt: MasterPasswordSalt;
|
||||
kdf: KdfConfig;
|
||||
masterPasswordAuthenticationHash: MasterPasswordAuthenticationHash;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PinKey, UserKey } from "@bitwarden/common/types/key";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { PinLockType } from "../services";
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { PinKey, UserKey } from "../../types/key";
|
||||
|
||||
import { PinLockType } from "./pin.service.implementation";
|
||||
|
||||
/**
|
||||
* The PinService is used for PIN-based unlocks. Below is a very basic overview of the PIN flow:
|
||||
@@ -2,26 +2,20 @@
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncString,
|
||||
EncryptedString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
PIN_DISK,
|
||||
PIN_MEMORY,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PinKey, UserKey } from "@bitwarden/common/types/key";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KdfConfig, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import { PinServiceAbstraction } from "../../abstractions/pin.service.abstraction";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString, EncryptedString } from "../../key-management/crypto/models/enc-string";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { PIN_DISK, PIN_MEMORY, StateProvider, UserKeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { PinKey, UserKey } from "../../types/key";
|
||||
|
||||
import { PinServiceAbstraction } from "./pin.service.abstraction";
|
||||
|
||||
/**
|
||||
* - DISABLED : No PIN set.
|
||||
@@ -1,21 +1,19 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
FakeAccountService,
|
||||
FakeStateProvider,
|
||||
mockAccountServiceWith,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PinKey, UserKey } from "@bitwarden/common/types/key";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
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 { CryptoFunctionService } from "../crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "../crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "../crypto/models/enc-string";
|
||||
|
||||
import {
|
||||
PinService,
|
||||
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user