mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-03 13:33:32 +01:00
[PM-15061] extract encryptors from generator service (#12068)
* introduce legacy encryptor provider * port credential generation service to encryptor provider
This commit is contained in:
parent
927c2fce43
commit
ab21b78c53
@ -0,0 +1,492 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject, Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||||
|
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import { OrganizationBound, UserBound } from "../dependencies";
|
||||||
|
|
||||||
|
import { KeyServiceLegacyEncryptorProvider } from "./key-service-legacy-encryptor-provider";
|
||||||
|
import { OrganizationEncryptor } from "./organization-encryptor.abstraction";
|
||||||
|
import { OrganizationKeyEncryptor } from "./organization-key-encryptor";
|
||||||
|
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||||
|
import { UserKeyEncryptor } from "./user-key-encryptor";
|
||||||
|
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
const keyService = mock<KeyService>();
|
||||||
|
|
||||||
|
const SomeCsprngArray = new Uint8Array(64) as CsprngArray;
|
||||||
|
const SomeUser = "some user" as UserId;
|
||||||
|
const AnotherUser = "another user" as UserId;
|
||||||
|
const SomeUserKey = new SymmetricCryptoKey(SomeCsprngArray) as UserKey;
|
||||||
|
const SomeOrganization = "some organization" as OrganizationId;
|
||||||
|
const AnotherOrganization = "another organization" as OrganizationId;
|
||||||
|
const SomeOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||||
|
const AnotherOrgKey = new SymmetricCryptoKey(SomeCsprngArray) as OrgKey;
|
||||||
|
const OrgRecords: Record<OrganizationId, OrgKey> = {
|
||||||
|
[SomeOrganization]: SomeOrgKey,
|
||||||
|
[AnotherOrganization]: AnotherOrgKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Many tests examine the private members of the objects constructed by the
|
||||||
|
// provider. This is necessary because it's not presently possible to spy
|
||||||
|
// on the constructors directly.
|
||||||
|
describe("KeyServiceLegacyEncryptorProvider", () => {
|
||||||
|
describe("userEncryptor$", () => {
|
||||||
|
it("emits a user key encryptor bound to the user", async () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: UserBound<"encryptor", UserEncryptor>[] = [];
|
||||||
|
|
||||||
|
provider.userEncryptor$(1, { singleUserId$ }).subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
expect(keyService.userKey$).toHaveBeenCalledWith(SomeUser);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0]).toMatchObject({
|
||||||
|
userId: SomeUser,
|
||||||
|
encryptor: {
|
||||||
|
userId: SomeUser,
|
||||||
|
key: SomeUserKey,
|
||||||
|
dataPacker: { frameSize: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results[0].encryptor).toBeInstanceOf(UserKeyEncryptor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits until `dependencies.singleUserId$` emits", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new Subject<UserId>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: UserBound<"encryptor", UserEncryptor>[] = [];
|
||||||
|
provider.userEncryptor$(1, { singleUserId$ }).subscribe((v) => results.push(v));
|
||||||
|
// precondition: no emissions occur on subscribe
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
|
||||||
|
singleUserId$.next(SomeUser);
|
||||||
|
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a new user key encryptor each time `dependencies.singleUserId$` emits", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new Subject<UserId>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: UserBound<"encryptor", UserEncryptor>[] = [];
|
||||||
|
provider.userEncryptor$(1, { singleUserId$ }).subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
singleUserId$.next(SomeUser);
|
||||||
|
singleUserId$.next(SomeUser);
|
||||||
|
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
expect(results[0]).not.toBe(results[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits until `userKey$` emits a truthy value", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(null);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: UserBound<"encryptor", UserEncryptor>[] = [];
|
||||||
|
provider.userEncryptor$(1, { singleUserId$ }).subscribe((v) => results.push(v));
|
||||||
|
// precondition: no emissions occur on subscribe
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
|
||||||
|
userKey$.next(SomeUserKey);
|
||||||
|
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0]).toMatchObject({
|
||||||
|
userId: SomeUser,
|
||||||
|
encryptor: {
|
||||||
|
userId: SomeUser,
|
||||||
|
key: SomeUserKey,
|
||||||
|
dataPacker: { frameSize: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a user key encryptor each time `userKey$` emits", () => {
|
||||||
|
const userKey$ = new Subject<UserKey>();
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: UserBound<"encryptor", UserEncryptor>[] = [];
|
||||||
|
provider.userEncryptor$(1, { singleUserId$ }).subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
userKey$.next(SomeUserKey);
|
||||||
|
userKey$.next(SomeUserKey);
|
||||||
|
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the userId changes", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new Subject<UserId>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.userEncryptor$(1, { singleUserId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
singleUserId$.next(SomeUser);
|
||||||
|
singleUserId$.next(AnotherUser);
|
||||||
|
|
||||||
|
expect(error).toEqual({ expectedUserId: SomeUser, actualUserId: AnotherUser });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when `dependencies.singleUserId$` errors", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new Subject<UserId>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.userEncryptor$(1, { singleUserId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
singleUserId$.error({ some: "error" });
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors once `dependencies.singleUserId$` emits and `userKey$` errors", () => {
|
||||||
|
const userKey$ = new Subject<UserKey>();
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.userEncryptor$(1, { singleUserId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
userKey$.error({ some: "error" });
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `dependencies.singleUserId$` completes", () => {
|
||||||
|
const userKey$ = new Subject<UserKey>();
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let completed = false;
|
||||||
|
provider
|
||||||
|
.userEncryptor$(1, { singleUserId$ })
|
||||||
|
.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
singleUserId$.complete();
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `userKey$` emits a falsy value after emitting a truthy value", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let completed = false;
|
||||||
|
provider
|
||||||
|
.userEncryptor$(1, { singleUserId$ })
|
||||||
|
.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
userKey$.next(null);
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes once `dependencies.singleUserId$` emits and `userKey$` completes", () => {
|
||||||
|
const userKey$ = new BehaviorSubject<UserKey>(SomeUserKey);
|
||||||
|
keyService.userKey$.mockReturnValue(userKey$);
|
||||||
|
const singleUserId$ = new BehaviorSubject<UserId>(SomeUser);
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let completed = false;
|
||||||
|
provider
|
||||||
|
.userEncryptor$(1, { singleUserId$ })
|
||||||
|
.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
userKey$.complete();
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("organizationEncryptor$", () => {
|
||||||
|
it("emits an organization key encryptor bound to the organization", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: OrganizationBound<"encryptor", OrganizationEncryptor>[] = [];
|
||||||
|
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
expect(keyService.orgKeys$).toHaveBeenCalledWith(SomeUser);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0]).toMatchObject({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
encryptor: {
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
key: SomeOrgKey,
|
||||||
|
dataPacker: { frameSize: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(results[0].encryptor).toBeInstanceOf(OrganizationKeyEncryptor);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits until `dependencies.singleOrganizationId$` emits", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new Subject<UserBound<"organizationId", OrganizationId>>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: OrganizationBound<"encryptor", OrganizationEncryptor>[] = [];
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe((v) => results.push(v));
|
||||||
|
// precondition: no emissions occur on subscribe
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
|
||||||
|
singleOrganizationId$.next({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits a new organization key encryptor when `dependencies.singleOrganizationId$` emits", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new Subject<UserBound<"organizationId", OrganizationId>>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: OrganizationBound<"encryptor", OrganizationEncryptor>[] = [];
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe((v) => results.push(v));
|
||||||
|
// precondition: no emissions occur on subscribe
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
|
||||||
|
singleOrganizationId$.next({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
singleOrganizationId$.next({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
expect(results[0]).not.toBe(results[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("waits until `orgKeys$` emits a truthy value", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject<Record<OrganizationId, OrgKey>>(null);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: OrganizationBound<"encryptor", OrganizationEncryptor>[] = [];
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe((v) => results.push(v));
|
||||||
|
// precondition: no emissions occur on subscribe
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
|
||||||
|
orgKey$.next(OrgRecords);
|
||||||
|
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0]).toMatchObject({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
encryptor: {
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
key: SomeOrgKey,
|
||||||
|
dataPacker: { frameSize: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits an organization key encryptor each time `orgKeys$` emits", () => {
|
||||||
|
const orgKey$ = new Subject<Record<OrganizationId, OrgKey>>();
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
const results: OrganizationBound<"encryptor", OrganizationEncryptor>[] = [];
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
orgKey$.next(OrgRecords);
|
||||||
|
orgKey$.next(OrgRecords);
|
||||||
|
|
||||||
|
expect(results.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the userId changes", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new Subject<UserBound<"organizationId", OrganizationId>>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
singleOrganizationId$.next({ userId: SomeUser, organizationId: SomeOrganization });
|
||||||
|
singleOrganizationId$.next({ userId: AnotherUser, organizationId: SomeOrganization });
|
||||||
|
|
||||||
|
expect(error).toEqual({ expectedUserId: SomeUser, actualUserId: AnotherUser });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the organizationId changes", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new Subject<UserBound<"organizationId", OrganizationId>>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
singleOrganizationId$.next({ userId: SomeUser, organizationId: SomeOrganization });
|
||||||
|
singleOrganizationId$.next({ userId: SomeUser, organizationId: AnotherOrganization });
|
||||||
|
|
||||||
|
expect(error).toEqual({
|
||||||
|
expectedOrganizationId: SomeOrganization,
|
||||||
|
actualOrganizationId: AnotherOrganization,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when `dependencies.singleOrganizationId$` errors", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new Subject<UserBound<"organizationId", OrganizationId>>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
singleOrganizationId$.error({ some: "error" });
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors once `dependencies.singleOrganizationId$` emits and `orgKeys$` errors", () => {
|
||||||
|
const orgKey$ = new Subject<Record<OrganizationId, OrgKey>>();
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
orgKey$.error({ some: "error" });
|
||||||
|
|
||||||
|
expect(error).toEqual({ some: "error" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the user lacks the requested org key", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject<Record<OrganizationId, OrgKey>>({});
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let error: unknown = false;
|
||||||
|
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ error: (e: unknown) => (error = e) });
|
||||||
|
|
||||||
|
expect(error).toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `dependencies.singleOrganizationId$` completes", () => {
|
||||||
|
const orgKey$ = new BehaviorSubject(OrgRecords);
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new Subject<UserBound<"organizationId", OrganizationId>>();
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let completed = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
singleOrganizationId$.complete();
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when `orgKeys$` emits a falsy value after emitting a truthy value", () => {
|
||||||
|
const orgKey$ = new Subject<Record<OrganizationId, OrgKey>>();
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let completed = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
orgKey$.next(OrgRecords);
|
||||||
|
orgKey$.next(null);
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes once `dependencies.singleOrganizationId$` emits and `userKey$` completes", () => {
|
||||||
|
const orgKey$ = new Subject<Record<OrganizationId, OrgKey>>();
|
||||||
|
keyService.orgKeys$.mockReturnValue(orgKey$);
|
||||||
|
const singleOrganizationId$ = new BehaviorSubject<
|
||||||
|
UserBound<"organizationId", OrganizationId>
|
||||||
|
>({
|
||||||
|
organizationId: SomeOrganization,
|
||||||
|
userId: SomeUser,
|
||||||
|
});
|
||||||
|
const provider = new KeyServiceLegacyEncryptorProvider(encryptService, keyService);
|
||||||
|
let completed = false;
|
||||||
|
provider
|
||||||
|
.organizationEncryptor$(1, { singleOrganizationId$ })
|
||||||
|
.subscribe({ complete: () => (completed = true) });
|
||||||
|
|
||||||
|
orgKey$.complete();
|
||||||
|
|
||||||
|
expect(completed).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
connect,
|
||||||
|
dematerialize,
|
||||||
|
map,
|
||||||
|
materialize,
|
||||||
|
ReplaySubject,
|
||||||
|
skipWhile,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
takeWhile,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrganizationBound,
|
||||||
|
SingleOrganizationDependency,
|
||||||
|
SingleUserDependency,
|
||||||
|
UserBound,
|
||||||
|
} from "../dependencies";
|
||||||
|
import { anyComplete, errorOnChange } from "../rx";
|
||||||
|
import { PaddedDataPacker } from "../state/padded-data-packer";
|
||||||
|
|
||||||
|
import { LegacyEncryptorProvider } from "./legacy-encryptor-provider";
|
||||||
|
import { OrganizationEncryptor } from "./organization-encryptor.abstraction";
|
||||||
|
import { OrganizationKeyEncryptor } from "./organization-key-encryptor";
|
||||||
|
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||||
|
import { UserKeyEncryptor } from "./user-key-encryptor";
|
||||||
|
|
||||||
|
/** Creates encryptors
|
||||||
|
*/
|
||||||
|
export class KeyServiceLegacyEncryptorProvider implements LegacyEncryptorProvider {
|
||||||
|
/** Instantiates the legacy encryptor provider.
|
||||||
|
* @param encryptService injected into encryptors to perform encryption
|
||||||
|
* @param keyService looks up keys for construction into an encryptor
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly encryptService: EncryptService,
|
||||||
|
private readonly keyService: KeyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
userEncryptor$(frameSize: number, dependencies: SingleUserDependency) {
|
||||||
|
const packer = new PaddedDataPacker(frameSize);
|
||||||
|
const encryptor$ = dependencies.singleUserId$.pipe(
|
||||||
|
errorOnChange(
|
||||||
|
(userId) => userId,
|
||||||
|
(expectedUserId, actualUserId) => ({ expectedUserId, actualUserId }),
|
||||||
|
),
|
||||||
|
connect((singleUserId$) => {
|
||||||
|
const singleUserId = new ReplaySubject<UserId>(1);
|
||||||
|
singleUserId$.subscribe(singleUserId);
|
||||||
|
|
||||||
|
return singleUserId.pipe(
|
||||||
|
switchMap((userId) =>
|
||||||
|
this.keyService.userKey$(userId).pipe(
|
||||||
|
// wait until the key becomes available
|
||||||
|
skipWhile((key) => !key),
|
||||||
|
// complete when the key becomes unavailable
|
||||||
|
takeWhile((key) => !!key),
|
||||||
|
map((key) => {
|
||||||
|
const encryptor = new UserKeyEncryptor(userId, this.encryptService, key, packer);
|
||||||
|
|
||||||
|
return { userId, encryptor } satisfies UserBound<"encryptor", UserEncryptor>;
|
||||||
|
}),
|
||||||
|
materialize(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dematerialize(),
|
||||||
|
takeUntil(anyComplete(singleUserId)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return encryptor$;
|
||||||
|
}
|
||||||
|
|
||||||
|
organizationEncryptor$(frameSize: number, dependencies: SingleOrganizationDependency) {
|
||||||
|
const packer = new PaddedDataPacker(frameSize);
|
||||||
|
const encryptor$ = dependencies.singleOrganizationId$.pipe(
|
||||||
|
errorOnChange(
|
||||||
|
(pair) => pair.userId,
|
||||||
|
(expectedUserId, actualUserId) => ({ expectedUserId, actualUserId }),
|
||||||
|
),
|
||||||
|
errorOnChange(
|
||||||
|
(pair) => pair.organizationId,
|
||||||
|
(expectedOrganizationId, actualOrganizationId) => ({
|
||||||
|
expectedOrganizationId,
|
||||||
|
actualOrganizationId,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
connect((singleOrganizationId$) => {
|
||||||
|
const singleOrganizationId = new ReplaySubject<UserBound<"organizationId", OrganizationId>>(
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
singleOrganizationId$.subscribe(singleOrganizationId);
|
||||||
|
|
||||||
|
return singleOrganizationId.pipe(
|
||||||
|
switchMap((pair) =>
|
||||||
|
this.keyService.orgKeys$(pair.userId).pipe(
|
||||||
|
// wait until the key becomes available
|
||||||
|
skipWhile((keys) => !keys),
|
||||||
|
// complete when the key becomes unavailable
|
||||||
|
takeWhile((keys) => !!keys),
|
||||||
|
map((keys) => {
|
||||||
|
const organizationId = pair.organizationId;
|
||||||
|
const key = keys[organizationId];
|
||||||
|
const encryptor = new OrganizationKeyEncryptor(
|
||||||
|
organizationId,
|
||||||
|
this.encryptService,
|
||||||
|
key,
|
||||||
|
packer,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { organizationId, encryptor } satisfies OrganizationBound<
|
||||||
|
"encryptor",
|
||||||
|
OrganizationEncryptor
|
||||||
|
>;
|
||||||
|
}),
|
||||||
|
materialize(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dematerialize(),
|
||||||
|
takeUntil(anyComplete(singleOrganizationId)),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return encryptor$;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrganizationBound,
|
||||||
|
SingleOrganizationDependency,
|
||||||
|
SingleUserDependency,
|
||||||
|
UserBound,
|
||||||
|
} from "../dependencies";
|
||||||
|
|
||||||
|
import { OrganizationEncryptor } from "./organization-encryptor.abstraction";
|
||||||
|
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||||
|
|
||||||
|
/** Creates encryptors
|
||||||
|
* @deprecated this logic will soon be replaced with a design that provides for
|
||||||
|
* key rotation. Use it at your own risk
|
||||||
|
*/
|
||||||
|
export abstract class LegacyEncryptorProvider {
|
||||||
|
/** Retrieves an encryptor populated with the user's most recent key instance that
|
||||||
|
* uses a padded data packer to encode data.
|
||||||
|
* @param frameSize length of the padded data packer's frames.
|
||||||
|
* @param dependencies.singleUserId$ identifies the user to which the encryptor is bound
|
||||||
|
* @returns an observable that emits when the key becomes available and completes
|
||||||
|
* when the key becomes unavailable.
|
||||||
|
*/
|
||||||
|
userEncryptor$: (
|
||||||
|
frameSize: number,
|
||||||
|
dependencies: SingleUserDependency,
|
||||||
|
) => Observable<UserBound<"encryptor", UserEncryptor>>;
|
||||||
|
|
||||||
|
/** Retrieves an encryptor populated with the organization's most recent key instance that
|
||||||
|
* uses a padded data packer to encode data.
|
||||||
|
* @param frameSize length of the padded data packer's frames.
|
||||||
|
* @param dependencies.singleOrganizationId$ identifies the user/org combination
|
||||||
|
* to which the encryptor is bound.
|
||||||
|
* @returns an observable that emits when the key becomes available and completes
|
||||||
|
* when the key becomes unavailable.
|
||||||
|
*/
|
||||||
|
organizationEncryptor$: (
|
||||||
|
frameSize: number,
|
||||||
|
dependences: SingleOrganizationDependency,
|
||||||
|
) => Observable<OrganizationBound<"encryptor", OrganizationEncryptor>>;
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
|
||||||
|
/** An encryption strategy that protects a type's secrets with
|
||||||
|
* organization-specific keys. This strategy is bound to a specific organization.
|
||||||
|
*/
|
||||||
|
export abstract class OrganizationEncryptor {
|
||||||
|
/** Identifies the organization bound to the encryptor. */
|
||||||
|
readonly organizationId: OrganizationId;
|
||||||
|
|
||||||
|
/** Protects secrets in `value` with an organization-specific key.
|
||||||
|
* @param secret the object to protect. This object is mutated during encryption.
|
||||||
|
* @returns a promise that resolves to a tuple. The tuple's first property contains
|
||||||
|
* the encrypted secret and whose second property contains an object w/ disclosed
|
||||||
|
* properties.
|
||||||
|
* @throws If `value` is `null` or `undefined`, the promise rejects with an error.
|
||||||
|
*/
|
||||||
|
abstract encrypt<Secret>(secret: Jsonify<Secret>): Promise<EncString>;
|
||||||
|
|
||||||
|
/** Combines protected secrets and disclosed data into a type that can be
|
||||||
|
* rehydrated into a domain object.
|
||||||
|
* @param secret an encrypted JSON payload containing encrypted secrets.
|
||||||
|
* @returns a promise that resolves to the raw state. This state *is not* a
|
||||||
|
* class. It contains only data that can be round-tripped through JSON,
|
||||||
|
* and lacks members such as a prototype or bound functions.
|
||||||
|
* @throws If `secret` or `disclosed` is `null` or `undefined`, the promise
|
||||||
|
* rejects with an error.
|
||||||
|
*/
|
||||||
|
abstract decrypt<Secret>(secret: EncString): Promise<Jsonify<Secret>>;
|
||||||
|
}
|
@ -0,0 +1,125 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||||
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { CsprngArray } from "../../types/csprng";
|
||||||
|
import { OrganizationId } from "../../types/guid";
|
||||||
|
import { OrgKey } from "../../types/key";
|
||||||
|
import { DataPacker } from "../state/data-packer.abstraction";
|
||||||
|
|
||||||
|
import { OrganizationKeyEncryptor } from "./organization-key-encryptor";
|
||||||
|
|
||||||
|
describe("OrgKeyEncryptor", () => {
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
const dataPacker = mock<DataPacker>();
|
||||||
|
const orgKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as OrgKey;
|
||||||
|
const anyOrgId = "foo" as OrganizationId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// The OrgKeyEncryptor is, in large part, a facade coordinating a handful of worker
|
||||||
|
// objects, so its tests focus on how data flows between components. The defaults rely
|
||||||
|
// on this property--that the facade treats its data like a opaque objects--to trace
|
||||||
|
// the data through several function calls. Should the encryptor interact with the
|
||||||
|
// objects themselves, these mocks will break.
|
||||||
|
encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString));
|
||||||
|
encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c as unknown as string));
|
||||||
|
dataPacker.pack.mockImplementation((v) => v as string);
|
||||||
|
dataPacker.unpack.mockImplementation(<T>(v: string) => v as T);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("should set organizationId", async () => {
|
||||||
|
const encryptor = new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, dataPacker);
|
||||||
|
expect(encryptor.organizationId).toEqual(anyOrgId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if organizationId was not supplied", async () => {
|
||||||
|
expect(() => new OrganizationKeyEncryptor(null, encryptService, orgKey, dataPacker)).toThrow(
|
||||||
|
"organizationId cannot be null or undefined",
|
||||||
|
);
|
||||||
|
expect(() => new OrganizationKeyEncryptor(null, encryptService, orgKey, dataPacker)).toThrow(
|
||||||
|
"organizationId cannot be null or undefined",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if encryptService was not supplied", async () => {
|
||||||
|
expect(() => new OrganizationKeyEncryptor(anyOrgId, null, orgKey, dataPacker)).toThrow(
|
||||||
|
"encryptService cannot be null or undefined",
|
||||||
|
);
|
||||||
|
expect(() => new OrganizationKeyEncryptor(anyOrgId, null, orgKey, dataPacker)).toThrow(
|
||||||
|
"encryptService cannot be null or undefined",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if key was not supplied", async () => {
|
||||||
|
expect(
|
||||||
|
() => new OrganizationKeyEncryptor(anyOrgId, encryptService, null, dataPacker),
|
||||||
|
).toThrow("key cannot be null or undefined");
|
||||||
|
expect(
|
||||||
|
() => new OrganizationKeyEncryptor(anyOrgId, encryptService, null, dataPacker),
|
||||||
|
).toThrow("key cannot be null or undefined");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw if dataPacker was not supplied", async () => {
|
||||||
|
expect(() => new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, null)).toThrow(
|
||||||
|
"dataPacker cannot be null or undefined",
|
||||||
|
);
|
||||||
|
expect(() => new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, null)).toThrow(
|
||||||
|
"dataPacker cannot be null or undefined",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("encrypt", () => {
|
||||||
|
it("should throw if value was not supplied", async () => {
|
||||||
|
const encryptor = new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, dataPacker);
|
||||||
|
|
||||||
|
await expect(encryptor.encrypt<Record<string, never>>(null)).rejects.toThrow(
|
||||||
|
"secret cannot be null or undefined",
|
||||||
|
);
|
||||||
|
await expect(encryptor.encrypt<Record<string, never>>(undefined)).rejects.toThrow(
|
||||||
|
"secret cannot be null or undefined",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should encrypt a packed value using the organization's key", async () => {
|
||||||
|
const encryptor = new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, dataPacker);
|
||||||
|
const value = { foo: true };
|
||||||
|
|
||||||
|
const result = await encryptor.encrypt(value);
|
||||||
|
|
||||||
|
// these are data flow expectations; the operations all all pass-through mocks
|
||||||
|
expect(dataPacker.pack).toHaveBeenCalledWith(value);
|
||||||
|
expect(encryptService.encrypt).toHaveBeenCalledWith(value, orgKey);
|
||||||
|
expect(result).toBe(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decrypt", () => {
|
||||||
|
it("should throw if secret was not supplied", async () => {
|
||||||
|
const encryptor = new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, dataPacker);
|
||||||
|
|
||||||
|
await expect(encryptor.decrypt(null)).rejects.toThrow("secret cannot be null or undefined");
|
||||||
|
await expect(encryptor.decrypt(undefined)).rejects.toThrow(
|
||||||
|
"secret cannot be null or undefined",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should declassify a decrypted packed value using the organization's key", async () => {
|
||||||
|
const encryptor = new OrganizationKeyEncryptor(anyOrgId, encryptService, orgKey, dataPacker);
|
||||||
|
const secret = "encrypted" as any;
|
||||||
|
|
||||||
|
const result = await encryptor.decrypt(secret);
|
||||||
|
|
||||||
|
// these are data flow expectations; the operations all all pass-through mocks
|
||||||
|
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(secret, orgKey);
|
||||||
|
expect(dataPacker.unpack).toHaveBeenCalledWith(secret);
|
||||||
|
expect(result).toBe(secret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||||
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
|
import { OrgKey } from "../../types/key";
|
||||||
|
import { DataPacker } from "../state/data-packer.abstraction";
|
||||||
|
|
||||||
|
import { OrganizationEncryptor } from "./organization-encryptor.abstraction";
|
||||||
|
|
||||||
|
/** A classification strategy that protects a type's secrets by encrypting them
|
||||||
|
* with an `OrgKey`
|
||||||
|
*/
|
||||||
|
export class OrganizationKeyEncryptor extends OrganizationEncryptor {
|
||||||
|
/** Instantiates the encryptor
|
||||||
|
* @param organizationId identifies the organization bound to the encryptor.
|
||||||
|
* @param encryptService protects properties of `Secret`.
|
||||||
|
* @param key the key instance protecting the data.
|
||||||
|
* @param dataPacker packs and unpacks data classified as secrets.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
readonly organizationId: OrganizationId,
|
||||||
|
private readonly encryptService: EncryptService,
|
||||||
|
private readonly key: OrgKey,
|
||||||
|
private readonly dataPacker: DataPacker,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.assertHasValue("organizationId", organizationId);
|
||||||
|
this.assertHasValue("key", key);
|
||||||
|
this.assertHasValue("dataPacker", dataPacker);
|
||||||
|
this.assertHasValue("encryptService", encryptService);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt<Secret>(secret: Jsonify<Secret>): Promise<EncString> {
|
||||||
|
this.assertHasValue("secret", secret);
|
||||||
|
|
||||||
|
let packed = this.dataPacker.pack(secret);
|
||||||
|
const encrypted = await this.encryptService.encrypt(packed, this.key);
|
||||||
|
packed = null;
|
||||||
|
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt<Secret>(secret: EncString): Promise<Jsonify<Secret>> {
|
||||||
|
this.assertHasValue("secret", secret);
|
||||||
|
|
||||||
|
let decrypted = await this.encryptService.decryptToUtf8(secret, this.key);
|
||||||
|
const unpacked = this.dataPacker.unpack<Secret>(decrypted);
|
||||||
|
decrypted = null;
|
||||||
|
|
||||||
|
return unpacked;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertHasValue(name: string, value: any) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
throw new Error(`${name} cannot be null or undefined`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
|||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { UserKey } from "../../types/key";
|
import { UserKey } from "../../types/key";
|
||||||
|
import { DataPacker } from "../state/data-packer.abstraction";
|
||||||
|
|
||||||
import { DataPacker } from "./data-packer.abstraction";
|
|
||||||
import { UserKeyEncryptor } from "./user-key-encryptor";
|
import { UserKeyEncryptor } from "./user-key-encryptor";
|
||||||
|
|
||||||
describe("UserKeyEncryptor", () => {
|
describe("UserKeyEncryptor", () => {
|
@ -5,8 +5,8 @@ import { UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { UserKey } from "../../types/key";
|
import { UserKey } from "../../types/key";
|
||||||
|
import { DataPacker } from "../state/data-packer.abstraction";
|
||||||
|
|
||||||
import { DataPacker } from "./data-packer.abstraction";
|
|
||||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
import { UserEncryptor } from "./user-encryptor.abstraction";
|
||||||
|
|
||||||
/** A classification strategy that protects a type's secrets by encrypting them
|
/** A classification strategy that protects a type's secrets by encrypting them
|
@ -1,9 +1,10 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { UserEncryptor } from "./state/user-encryptor.abstraction";
|
import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction";
|
||||||
|
import { UserEncryptor } from "./cryptography/user-encryptor.abstraction";
|
||||||
|
|
||||||
/** error emitted when the `SingleUserDependency` changes Ids */
|
/** error emitted when the `SingleUserDependency` changes Ids */
|
||||||
export type UserChangedError = {
|
export type UserChangedError = {
|
||||||
@ -13,6 +14,14 @@ export type UserChangedError = {
|
|||||||
actualUserId: UserId;
|
actualUserId: UserId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** error emitted when the `SingleOrganizationDependency` changes Ids */
|
||||||
|
export type OrganizationChangedError = {
|
||||||
|
/** the organizationId pinned by the single organization dependency */
|
||||||
|
expectedOrganizationId: OrganizationId;
|
||||||
|
/** the organizationId received in error */
|
||||||
|
actualOrganizationId: OrganizationId;
|
||||||
|
};
|
||||||
|
|
||||||
/** A pattern for types that depend upon a dynamic policy stream and return
|
/** A pattern for types that depend upon a dynamic policy stream and return
|
||||||
* an observable.
|
* an observable.
|
||||||
*
|
*
|
||||||
@ -55,6 +64,54 @@ export type UserBound<K extends keyof any, T> = { [P in K]: T } & {
|
|||||||
userId: UserId;
|
userId: UserId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Decorates a type to indicate the organization, if any, that the type is usable only by
|
||||||
|
* a specific organization.
|
||||||
|
*/
|
||||||
|
export type OrganizationBound<K extends keyof any, T> = { [P in K]: T } & {
|
||||||
|
/** The organization to which T is bound. */
|
||||||
|
organizationId: OrganizationId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A pattern for types that depend upon a fixed-key encryptor and return
|
||||||
|
* an observable.
|
||||||
|
*
|
||||||
|
* Consumers of this dependency should emit a `OrganizationChangedError` if
|
||||||
|
* the bound OrganizationId changes or if the encryptor changes. If
|
||||||
|
* `singleOrganizationEncryptor$` completes, the consumer should complete
|
||||||
|
* once all events received prior to the completion event are
|
||||||
|
* finished processing. The consumer should, where possible,
|
||||||
|
* prioritize these events in order to complete as soon as possible.
|
||||||
|
* If `singleOrganizationEncryptor$` emits an unrecoverable error, the consumer
|
||||||
|
* should also emit the error.
|
||||||
|
*/
|
||||||
|
export type SingleOrganizationEncryptorDependency = {
|
||||||
|
/** A stream that emits an encryptor when subscribed and the org key
|
||||||
|
* is available, and completes when the org key is no longer available.
|
||||||
|
* The stream should not emit null or undefined.
|
||||||
|
*/
|
||||||
|
singleOrgEncryptor$: Observable<OrganizationBound<"encryptor", OrganizationEncryptor>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A pattern for types that depend upon a fixed-value organizationId and return
|
||||||
|
* an observable.
|
||||||
|
*
|
||||||
|
* Consumers of this dependency should emit a `OrganizationChangedError` if
|
||||||
|
* the value of `singleOrganizationId$` changes. If `singleOrganizationId$` completes,
|
||||||
|
* the consumer should also complete. If `singleOrganizationId$` errors, the
|
||||||
|
* consumer should also emit the error.
|
||||||
|
*
|
||||||
|
* @remarks Check the consumer's documentation to determine how it
|
||||||
|
* responds to repeat emissions.
|
||||||
|
*/
|
||||||
|
export type SingleOrganizationDependency = {
|
||||||
|
/** A stream that emits an organization Id and the user to which it is bound
|
||||||
|
* when subscribed and the user's account is unlocked, and completes when the
|
||||||
|
* account is locked or logged out.
|
||||||
|
* The stream should not emit null or undefined.
|
||||||
|
*/
|
||||||
|
singleOrganizationId$: Observable<UserBound<"organizationId", OrganizationId>>;
|
||||||
|
};
|
||||||
|
|
||||||
/** A pattern for types that depend upon a fixed-key encryptor and return
|
/** A pattern for types that depend upon a fixed-key encryptor and return
|
||||||
* an observable.
|
* an observable.
|
||||||
*
|
*
|
||||||
|
@ -8,6 +8,7 @@ import { awaitAsync, trackEmissions } from "../../spec";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
anyComplete,
|
anyComplete,
|
||||||
|
errorOnChange,
|
||||||
distinctIfShallowMatch,
|
distinctIfShallowMatch,
|
||||||
on,
|
on,
|
||||||
ready,
|
ready,
|
||||||
@ -15,6 +16,104 @@ import {
|
|||||||
withLatestReady,
|
withLatestReady,
|
||||||
} from "./rx";
|
} from "./rx";
|
||||||
|
|
||||||
|
describe("errorOnChange", () => {
|
||||||
|
it("emits a single value when the input emits only once", async () => {
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(errorOnChange()).subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
source$.next(1);
|
||||||
|
|
||||||
|
expect(results).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits when the input emits", async () => {
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const results: number[] = [];
|
||||||
|
source$.pipe(errorOnChange()).subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
source$.next(1);
|
||||||
|
source$.next(1);
|
||||||
|
|
||||||
|
expect(results).toEqual([1, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the input errors", async () => {
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
const expected = {};
|
||||||
|
let error: any = null;
|
||||||
|
source$.pipe(errorOnChange()).subscribe({ error: (v: unknown) => (error = v) });
|
||||||
|
|
||||||
|
source$.error(expected);
|
||||||
|
|
||||||
|
expect(error).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("completes when the input completes", async () => {
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
let complete: boolean = false;
|
||||||
|
source$.pipe(errorOnChange()).subscribe({ complete: () => (complete = true) });
|
||||||
|
|
||||||
|
source$.complete();
|
||||||
|
|
||||||
|
expect(complete).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when the input changes", async () => {
|
||||||
|
const source$ = new Subject<number>();
|
||||||
|
let error: any = null;
|
||||||
|
source$.pipe(errorOnChange()).subscribe({ error: (v: unknown) => (error = v) });
|
||||||
|
|
||||||
|
source$.next(1);
|
||||||
|
source$.next(2);
|
||||||
|
|
||||||
|
expect(error).toEqual({ expectedValue: 1, actualValue: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits when the extracted value remains constant", async () => {
|
||||||
|
type Foo = { foo: string };
|
||||||
|
const source$ = new Subject<Foo>();
|
||||||
|
const results: Foo[] = [];
|
||||||
|
source$.pipe(errorOnChange((v) => v.foo)).subscribe((v) => results.push(v));
|
||||||
|
|
||||||
|
source$.next({ foo: "bar" });
|
||||||
|
source$.next({ foo: "bar" });
|
||||||
|
|
||||||
|
expect(results).toEqual([{ foo: "bar" }, { foo: "bar" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("errors when an extracted value changes", async () => {
|
||||||
|
type Foo = { foo: string };
|
||||||
|
const source$ = new Subject<Foo>();
|
||||||
|
let error: any = null;
|
||||||
|
source$.pipe(errorOnChange((v) => v.foo)).subscribe({ error: (v: unknown) => (error = v) });
|
||||||
|
|
||||||
|
source$.next({ foo: "bar" });
|
||||||
|
source$.next({ foo: "baz" });
|
||||||
|
|
||||||
|
expect(error).toEqual({ expectedValue: "bar", actualValue: "baz" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("constructs an error when the extracted value changes", async () => {
|
||||||
|
type Foo = { foo: string };
|
||||||
|
const source$ = new Subject<Foo>();
|
||||||
|
let error: any = null;
|
||||||
|
source$
|
||||||
|
.pipe(
|
||||||
|
errorOnChange(
|
||||||
|
(v) => v.foo,
|
||||||
|
(expected, actual) => ({ expected, actual }),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subscribe({ error: (v: unknown) => (error = v) });
|
||||||
|
|
||||||
|
source$.next({ foo: "bar" });
|
||||||
|
source$.next({ foo: "baz" });
|
||||||
|
|
||||||
|
expect(error).toEqual({ expected: "bar", actual: "baz" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("reduceCollection", () => {
|
describe("reduceCollection", () => {
|
||||||
it.each([[null], [undefined], [[]]])(
|
it.each([[null], [undefined], [[]]])(
|
||||||
"should return the default value when the collection is %p",
|
"should return the default value when the collection is %p",
|
||||||
|
@ -15,8 +15,60 @@ import {
|
|||||||
takeUntil,
|
takeUntil,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
concatMap,
|
concatMap,
|
||||||
|
startWith,
|
||||||
|
pairwise,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
/** Returns its input. */
|
||||||
|
function identity(value: any): any {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Combines its arguments into a plain old javascript object. */
|
||||||
|
function expectedAndActualValue(expectedValue: any, actualValue: any) {
|
||||||
|
return {
|
||||||
|
expectedValue,
|
||||||
|
actualValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable operator that throws an error when the stream's
|
||||||
|
* value changes. Uses strict (`===`) comparison checks.
|
||||||
|
* @param extract a function that identifies the member to compare;
|
||||||
|
* defaults to the identity function
|
||||||
|
* @param error a function that packages the expected and failed
|
||||||
|
* values into an error.
|
||||||
|
* @returns a stream of values that emits when the input emits,
|
||||||
|
* completes when the input completes, and errors when either the
|
||||||
|
* input errors or the comparison fails.
|
||||||
|
*/
|
||||||
|
export function errorOnChange<Input, Extracted>(
|
||||||
|
extract: (value: Input) => Extracted = identity,
|
||||||
|
error: (expectedValue: Extracted, actualValue: Extracted) => unknown = expectedAndActualValue,
|
||||||
|
): OperatorFunction<Input, Input> {
|
||||||
|
return pipe(
|
||||||
|
startWith(null),
|
||||||
|
pairwise(),
|
||||||
|
map(([expected, actual], i) => {
|
||||||
|
// always let the first value through
|
||||||
|
if (i === 0) {
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedValue = extract(expected);
|
||||||
|
const actualValue = extract(actual);
|
||||||
|
|
||||||
|
// fail the stream if the state desyncs from its initial value
|
||||||
|
if (expectedValue === actualValue) {
|
||||||
|
return actual;
|
||||||
|
} else {
|
||||||
|
throw error(expectedValue, actualValue);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable operator that reduces an emitted collection to a single object,
|
* An observable operator that reduces an emitted collection to a single object,
|
||||||
* returning a default if all items are ignored.
|
* returning a default if all items are ignored.
|
||||||
|
@ -5,6 +5,17 @@ import type { StateDefinition } from "../../platform/state/state-definition";
|
|||||||
import { ClassifiedFormat } from "./classified-format";
|
import { ClassifiedFormat } from "./classified-format";
|
||||||
import { Classifier } from "./classifier";
|
import { Classifier } from "./classifier";
|
||||||
|
|
||||||
|
/** Determines the format of persistent storage.
|
||||||
|
* `plain` storage is a plain-old javascript object. Use this type
|
||||||
|
* when you are performing your own encryption and decryption.
|
||||||
|
* `classified` uses the `ClassifiedFormat` type as its format.
|
||||||
|
* `secret-state` uses `Array<ClassifiedFormat>` with a length of 1.
|
||||||
|
* @remarks - CAUTION! If your on-disk data is not in a correct format,
|
||||||
|
* the storage system treats the data as corrupt and returns your initial
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
export type ObjectStorageFormat = "plain" | "classified" | "secret-state";
|
||||||
|
|
||||||
/** A key for storing JavaScript objects (`{ an: "example" }`)
|
/** A key for storing JavaScript objects (`{ an: "example" }`)
|
||||||
* in a UserStateSubject.
|
* in a UserStateSubject.
|
||||||
*/
|
*/
|
||||||
@ -20,7 +31,7 @@ export type ObjectKey<State, Secret = State, Disclosed = Record<string, never>>
|
|||||||
key: string;
|
key: string;
|
||||||
state: StateDefinition;
|
state: StateDefinition;
|
||||||
classifier: Classifier<State, Disclosed, Secret>;
|
classifier: Classifier<State, Disclosed, Secret>;
|
||||||
format: "plain" | "classified";
|
format: ObjectStorageFormat;
|
||||||
options: UserKeyDefinitionOptions<State>;
|
options: UserKeyDefinitionOptions<State>;
|
||||||
initial?: State;
|
initial?: State;
|
||||||
};
|
};
|
||||||
@ -47,6 +58,18 @@ export function toUserKeyDefinition<State, Secret, Disclosed>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return classified;
|
||||||
|
} else if (key.format === "secret-state") {
|
||||||
|
const classified = new UserKeyDefinition<[ClassifiedFormat<void, Disclosed>]>(
|
||||||
|
key.state,
|
||||||
|
key.key,
|
||||||
|
{
|
||||||
|
cleanupDelayMs: key.options.cleanupDelayMs,
|
||||||
|
deserializer: (jsonValue) => jsonValue as [ClassifiedFormat<void, Disclosed>],
|
||||||
|
clearOn: key.options.clearOn,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return classified;
|
return classified;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unknown format: ${key.format}`);
|
throw new Error(`unknown format: ${key.format}`);
|
||||||
|
@ -11,11 +11,11 @@ import {
|
|||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { GENERATOR_DISK } from "../../platform/state";
|
import { GENERATOR_DISK } from "../../platform/state";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||||
|
|
||||||
import { SecretClassifier } from "./secret-classifier";
|
import { SecretClassifier } from "./secret-classifier";
|
||||||
import { SecretKeyDefinition } from "./secret-key-definition";
|
import { SecretKeyDefinition } from "./secret-key-definition";
|
||||||
import { SecretState } from "./secret-state";
|
import { SecretState } from "./secret-state";
|
||||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
|
||||||
|
|
||||||
type FooBar = { foo: boolean; bar: boolean; date?: Date };
|
type FooBar = { foo: boolean; bar: boolean; date?: Date };
|
||||||
const classifier = SecretClassifier.allSecret<FooBar>();
|
const classifier = SecretClassifier.allSecret<FooBar>();
|
||||||
|
@ -8,10 +8,10 @@ import {
|
|||||||
CombinedState,
|
CombinedState,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||||
|
|
||||||
import { ClassifiedFormat } from "./classified-format";
|
import { ClassifiedFormat } from "./classified-format";
|
||||||
import { SecretKeyDefinition } from "./secret-key-definition";
|
import { SecretKeyDefinition } from "./secret-key-definition";
|
||||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
|
||||||
|
|
||||||
const ONE_MINUTE = 1000 * 60;
|
const ONE_MINUTE = 1000 * 60;
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/st
|
|||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec";
|
import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec";
|
||||||
|
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||||
import { UserBound } from "../dependencies";
|
import { UserBound } from "../dependencies";
|
||||||
import { PrivateClassifier } from "../private-classifier";
|
import { PrivateClassifier } from "../private-classifier";
|
||||||
import { StateConstraints } from "../types";
|
import { StateConstraints } from "../types";
|
||||||
|
|
||||||
import { ClassifiedFormat } from "./classified-format";
|
import { ClassifiedFormat } from "./classified-format";
|
||||||
import { ObjectKey } from "./object-key";
|
import { ObjectKey } from "./object-key";
|
||||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
|
||||||
import { UserStateSubject } from "./user-state-subject";
|
import { UserStateSubject } from "./user-state-subject";
|
||||||
|
|
||||||
const SomeUser = "some user" as UserId;
|
const SomeUser = "some user" as UserId;
|
||||||
@ -734,6 +734,7 @@ describe("UserStateSubject", () => {
|
|||||||
error = e as any;
|
error = e as any;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
singleUserEncryptor$.next({ userId: SomeUser, encryptor: SomeEncryptor });
|
||||||
singleUserEncryptor$.next({ userId: errorUserId, encryptor: SomeEncryptor });
|
singleUserEncryptor$.next({ userId: errorUserId, encryptor: SomeEncryptor });
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
|
|
||||||
|
@ -6,10 +6,8 @@ import {
|
|||||||
filter,
|
filter,
|
||||||
map,
|
map,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
pairwise,
|
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
startWith,
|
|
||||||
Observable,
|
Observable,
|
||||||
Subscription,
|
Subscription,
|
||||||
last,
|
last,
|
||||||
@ -30,15 +28,15 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|||||||
import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
import { UserEncryptor } from "../cryptography/user-encryptor.abstraction";
|
||||||
import { UserBound } from "../dependencies";
|
import { UserBound } from "../dependencies";
|
||||||
import { anyComplete, ready, withLatestReady } from "../rx";
|
import { anyComplete, errorOnChange, ready, withLatestReady } from "../rx";
|
||||||
import { Constraints, SubjectConstraints, WithConstraints } from "../types";
|
import { Constraints, SubjectConstraints, WithConstraints } from "../types";
|
||||||
|
|
||||||
import { ClassifiedFormat, isClassifiedFormat } from "./classified-format";
|
import { ClassifiedFormat, isClassifiedFormat } from "./classified-format";
|
||||||
import { unconstrained$ } from "./identity-state-constraint";
|
import { unconstrained$ } from "./identity-state-constraint";
|
||||||
import { isObjectKey, ObjectKey, toUserKeyDefinition } from "./object-key";
|
import { isObjectKey, ObjectKey, toUserKeyDefinition } from "./object-key";
|
||||||
import { isDynamic } from "./state-constraints-dependency";
|
import { isDynamic } from "./state-constraints-dependency";
|
||||||
import { UserEncryptor } from "./user-encryptor.abstraction";
|
|
||||||
import { UserStateSubjectDependencies } from "./user-state-subject-dependencies";
|
import { UserStateSubjectDependencies } from "./user-state-subject-dependencies";
|
||||||
|
|
||||||
type Constrained<State> = { constraints: Readonly<Constraints<State>>; state: State };
|
type Constrained<State> = { constraints: Readonly<Constraints<State>>; state: State };
|
||||||
@ -195,24 +193,13 @@ export class UserStateSubject<
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// fail the stream if the state desyncs from the bound userId
|
// fail the stream if the state desyncs from the bound userId
|
||||||
startWith({ userId: this.state.userId, encryptor: null } as UserBound<
|
errorOnChange(
|
||||||
"encryptor",
|
({ userId }) => userId,
|
||||||
UserEncryptor
|
(expectedUserId, actualUserId) => ({ expectedUserId, actualUserId }),
|
||||||
>),
|
),
|
||||||
pairwise(),
|
|
||||||
map(([expected, actual]) => {
|
|
||||||
if (expected.userId === actual.userId) {
|
|
||||||
return actual;
|
|
||||||
} else {
|
|
||||||
throw {
|
|
||||||
expectedUserId: expected.userId,
|
|
||||||
actualUserId: actual.userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// reduce emissions to when encryptor changes
|
// reduce emissions to when encryptor changes
|
||||||
distinctUntilChanged(),
|
|
||||||
map(({ encryptor }) => encryptor),
|
map(({ encryptor }) => encryptor),
|
||||||
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,36 +304,63 @@ export class UserStateSubject<
|
|||||||
return (input$) => input$ as Observable<State>;
|
return (input$) => input$ as Observable<State>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the key supports encryption, enable encryptor support
|
// all other keys support encryption; enable encryptor support
|
||||||
|
return pipe(
|
||||||
|
this.mapToClassifiedFormat(),
|
||||||
|
combineLatestWith(encryptor$),
|
||||||
|
concatMap(async ([input, encryptor]) => {
|
||||||
|
// pass through null values
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt classified data
|
||||||
|
const { secret, disclosed } = input;
|
||||||
|
const encrypted = EncString.fromJSON(secret);
|
||||||
|
const decryptedSecret = await encryptor.decrypt<Secret>(encrypted);
|
||||||
|
|
||||||
|
// assemble into proper state
|
||||||
|
const declassified = this.objectKey.classifier.declassify(disclosed, decryptedSecret);
|
||||||
|
const state = this.objectKey.options.deserializer(declassified);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapToClassifiedFormat(): OperatorFunction<unknown, ClassifiedFormat<unknown, unknown>> {
|
||||||
|
// FIXME: warn when data is dropped in the console and/or report an error
|
||||||
|
// through the observable; consider redirecting dropped data to a recovery
|
||||||
|
// location
|
||||||
|
|
||||||
|
// user-state subject's default format is object-aware
|
||||||
if (this.objectKey && this.objectKey.format === "classified") {
|
if (this.objectKey && this.objectKey.format === "classified") {
|
||||||
return pipe(
|
return map((input) => {
|
||||||
combineLatestWith(encryptor$),
|
if (!isClassifiedFormat(input)) {
|
||||||
concatMap(async ([input, encryptor]) => {
|
return null;
|
||||||
// pass through null values
|
}
|
||||||
if (input === null || input === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail fast if the format is incorrect
|
return input;
|
||||||
if (!isClassifiedFormat(input)) {
|
});
|
||||||
throw new Error(`Cannot declassify ${this.key.key}; unknown format.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrypt classified data
|
|
||||||
const { secret, disclosed } = input;
|
|
||||||
const encrypted = EncString.fromJSON(secret);
|
|
||||||
const decryptedSecret = await encryptor.decrypt<Secret>(encrypted);
|
|
||||||
|
|
||||||
// assemble into proper state
|
|
||||||
const declassified = this.objectKey.classifier.declassify(disclosed, decryptedSecret);
|
|
||||||
const state = this.objectKey.options.deserializer(declassified);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`unknown serialization format: ${this.objectKey.format}`);
|
// secret state's format wraps objects in an array
|
||||||
|
if (this.objectKey && this.objectKey.format === "secret-state") {
|
||||||
|
return map((input) => {
|
||||||
|
if (!Array.isArray(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [unwrapped] = input;
|
||||||
|
if (!isClassifiedFormat(unwrapped)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unwrapped;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`unsupported serialization format: ${this.objectKey.format}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private classify(encryptor$: Observable<UserEncryptor>): OperatorFunction<State, unknown> {
|
private classify(encryptor$: Observable<UserEncryptor>): OperatorFunction<State, unknown> {
|
||||||
@ -359,41 +373,49 @@ export class UserStateSubject<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the key supports encryption, enable encryptor support
|
// all other keys support encryption; enable encryptor support
|
||||||
|
return pipe(
|
||||||
|
withLatestReady(encryptor$),
|
||||||
|
concatMap(async ([input, encryptor]) => {
|
||||||
|
// fail fast if there's no value
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split data by classification level
|
||||||
|
const serialized = JSON.parse(JSON.stringify(input));
|
||||||
|
const classified = this.objectKey.classifier.classify(serialized);
|
||||||
|
|
||||||
|
// protect data
|
||||||
|
const encrypted = await encryptor.encrypt(classified.secret);
|
||||||
|
const secret = JSON.parse(JSON.stringify(encrypted));
|
||||||
|
|
||||||
|
// wrap result in classified format envelope for storage
|
||||||
|
const envelope = {
|
||||||
|
id: null as void,
|
||||||
|
secret,
|
||||||
|
disclosed: classified.disclosed,
|
||||||
|
} satisfies ClassifiedFormat<void, Disclosed>;
|
||||||
|
|
||||||
|
// deliberate type erasure; the type is restored during `declassify`
|
||||||
|
return envelope as ClassifiedFormat<unknown, unknown>;
|
||||||
|
}),
|
||||||
|
this.mapToStorageFormat(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapToStorageFormat(): OperatorFunction<ClassifiedFormat<unknown, unknown>, unknown> {
|
||||||
|
// user-state subject's default format is object-aware
|
||||||
if (this.objectKey && this.objectKey.format === "classified") {
|
if (this.objectKey && this.objectKey.format === "classified") {
|
||||||
return pipe(
|
return map((input) => input as unknown);
|
||||||
withLatestReady(encryptor$),
|
|
||||||
concatMap(async ([input, encryptor]) => {
|
|
||||||
// fail fast if there's no value
|
|
||||||
if (input === null || input === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// split data by classification level
|
|
||||||
const serialized = JSON.parse(JSON.stringify(input));
|
|
||||||
const classified = this.objectKey.classifier.classify(serialized);
|
|
||||||
|
|
||||||
// protect data
|
|
||||||
const encrypted = await encryptor.encrypt(classified.secret);
|
|
||||||
const secret = JSON.parse(JSON.stringify(encrypted));
|
|
||||||
|
|
||||||
// wrap result in classified format envelope for storage
|
|
||||||
const envelope = {
|
|
||||||
id: null as void,
|
|
||||||
secret,
|
|
||||||
disclosed: classified.disclosed,
|
|
||||||
} satisfies ClassifiedFormat<void, Disclosed>;
|
|
||||||
|
|
||||||
// deliberate type erasure; the type is restored during `declassify`
|
|
||||||
return envelope as unknown;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: add "encrypted" format --> key contains encryption logic
|
// secret state's format wraps objects in an array
|
||||||
// CONSIDER: should "classified format" algorithm be embedded in subject keys...?
|
if (this.objectKey && this.objectKey.format === "secret-state") {
|
||||||
|
return map((input) => [input] as unknown);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`unknown serialization format: ${this.objectKey.format}`);
|
throw new Error(`unsupported serialization format: ${this.objectKey.format}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The userId to which the subject is bound.
|
/** The userId to which the subject is bound.
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<bit-form-field *ngIf="displayToken">
|
<bit-form-field *ngIf="displayToken">
|
||||||
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
<bit-label>{{ "apiKey" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="token" type="password" />
|
<input bitInput formControlName="token" type="password" (change)="save('password')" />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton
|
bitIconButton
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||||
|
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||||
|
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||||
|
import {
|
||||||
|
createRandomizer,
|
||||||
|
CredentialGeneratorService,
|
||||||
|
Randomizer,
|
||||||
|
} from "@bitwarden/generator-core";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
export const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
||||||
|
|
||||||
|
/** Shared module containing generator component dependencies */
|
||||||
|
@NgModule({
|
||||||
|
imports: [JslibModule],
|
||||||
|
providers: [
|
||||||
|
safeProvider({
|
||||||
|
provide: RANDOMIZER,
|
||||||
|
useFactory: createRandomizer,
|
||||||
|
deps: [KeyService],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: LegacyEncryptorProvider,
|
||||||
|
useClass: KeyServiceLegacyEncryptorProvider,
|
||||||
|
deps: [EncryptService, KeyService],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CredentialGeneratorService,
|
||||||
|
useClass: CredentialGeneratorService,
|
||||||
|
deps: [
|
||||||
|
RANDOMIZER,
|
||||||
|
StateProvider,
|
||||||
|
PolicyService,
|
||||||
|
ApiService,
|
||||||
|
I18nService,
|
||||||
|
LegacyEncryptorProvider,
|
||||||
|
AccountService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class GeneratorServicesModule {
|
||||||
|
constructor() {}
|
||||||
|
}
|
@ -3,14 +3,6 @@ import { NgModule } from "@angular/core";
|
|||||||
import { ReactiveFormsModule } from "@angular/forms";
|
import { ReactiveFormsModule } from "@angular/forms";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
|
||||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
|
||||||
import {
|
import {
|
||||||
CardComponent,
|
CardComponent,
|
||||||
ColorPasswordModule,
|
ColorPasswordModule,
|
||||||
@ -25,16 +17,11 @@ import {
|
|||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import {
|
|
||||||
createRandomizer,
|
|
||||||
CredentialGeneratorService,
|
|
||||||
Randomizer,
|
|
||||||
} from "@bitwarden/generator-core";
|
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { CatchallSettingsComponent } from "./catchall-settings.component";
|
import { CatchallSettingsComponent } from "./catchall-settings.component";
|
||||||
import { CredentialGeneratorComponent } from "./credential-generator.component";
|
import { CredentialGeneratorComponent } from "./credential-generator.component";
|
||||||
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
|
import { ForwarderSettingsComponent } from "./forwarder-settings.component";
|
||||||
|
import { GeneratorServicesModule } from "./generator-services.module";
|
||||||
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
import { PassphraseSettingsComponent } from "./passphrase-settings.component";
|
||||||
import { PasswordGeneratorComponent } from "./password-generator.component";
|
import { PasswordGeneratorComponent } from "./password-generator.component";
|
||||||
import { PasswordSettingsComponent } from "./password-settings.component";
|
import { PasswordSettingsComponent } from "./password-settings.component";
|
||||||
@ -42,8 +29,6 @@ import { SubaddressSettingsComponent } from "./subaddress-settings.component";
|
|||||||
import { UsernameGeneratorComponent } from "./username-generator.component";
|
import { UsernameGeneratorComponent } from "./username-generator.component";
|
||||||
import { UsernameSettingsComponent } from "./username-settings.component";
|
import { UsernameSettingsComponent } from "./username-settings.component";
|
||||||
|
|
||||||
const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|
||||||
|
|
||||||
/** Shared module containing generator component dependencies */
|
/** Shared module containing generator component dependencies */
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@ -52,6 +37,7 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
CheckboxModule,
|
CheckboxModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
|
GeneratorServicesModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
InputModule,
|
InputModule,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
@ -63,27 +49,6 @@ const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|||||||
ToggleGroupModule,
|
ToggleGroupModule,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
],
|
],
|
||||||
providers: [
|
|
||||||
safeProvider({
|
|
||||||
provide: RANDOMIZER,
|
|
||||||
useFactory: createRandomizer,
|
|
||||||
deps: [KeyService],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
provide: CredentialGeneratorService,
|
|
||||||
useClass: CredentialGeneratorService,
|
|
||||||
deps: [
|
|
||||||
RANDOMIZER,
|
|
||||||
StateProvider,
|
|
||||||
PolicyService,
|
|
||||||
ApiService,
|
|
||||||
I18nService,
|
|
||||||
EncryptService,
|
|
||||||
KeyService,
|
|
||||||
AccountService,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
declarations: [
|
declarations: [
|
||||||
CatchallSettingsComponent,
|
CatchallSettingsComponent,
|
||||||
CredentialGeneratorComponent,
|
CredentialGeneratorComponent,
|
||||||
|
@ -2,3 +2,4 @@ export { CredentialGeneratorHistoryComponent } from "./credential-generator-hist
|
|||||||
export { CredentialGeneratorHistoryDialogComponent } from "./credential-generator-history-dialog.component";
|
export { CredentialGeneratorHistoryDialogComponent } from "./credential-generator-history-dialog.component";
|
||||||
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
export { EmptyCredentialHistoryComponent } from "./empty-credential-history.component";
|
||||||
export { GeneratorModule } from "./generator.module";
|
export { GeneratorModule } from "./generator.module";
|
||||||
|
export { GeneratorServicesModule } from "./generator-services.module";
|
||||||
|
@ -369,7 +369,7 @@ export function toCredentialGeneratorConfiguration<Settings extends ApiSettings
|
|||||||
settings: {
|
settings: {
|
||||||
initial: configuration.forwarder.defaultSettings,
|
initial: configuration.forwarder.defaultSettings,
|
||||||
constraints: configuration.forwarder.settingsConstraints,
|
constraints: configuration.forwarder.settingsConstraints,
|
||||||
account: configuration.forwarder.settings,
|
account: configuration.forwarder.local.settings,
|
||||||
},
|
},
|
||||||
policy: {
|
policy: {
|
||||||
type: PolicyType.PasswordGenerator,
|
type: PolicyType.PasswordGenerator,
|
||||||
|
@ -27,6 +27,7 @@ export type AddyIoConfiguration = ForwarderConfiguration<AddyIoSettings>;
|
|||||||
const defaultSettings = Object.freeze({
|
const defaultSettings = Object.freeze({
|
||||||
token: "",
|
token: "",
|
||||||
domain: "",
|
domain: "",
|
||||||
|
baseUrl: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// supported RPC calls
|
// supported RPC calls
|
||||||
@ -65,9 +66,10 @@ const forwarder = Object.freeze({
|
|||||||
// e.g. key: "forwarder.AddyIo.local.settings",
|
// e.g. key: "forwarder.AddyIo.local.settings",
|
||||||
key: "addyIoForwarder",
|
key: "addyIoForwarder",
|
||||||
target: "object",
|
target: "object",
|
||||||
format: "classified",
|
format: "secret-state",
|
||||||
classifier: new PrivateClassifier<AddyIoSettings>(),
|
classifier: new PrivateClassifier<AddyIoSettings>(),
|
||||||
state: GENERATOR_DISK,
|
state: GENERATOR_DISK,
|
||||||
|
initial: defaultSettings,
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
@ -55,9 +55,10 @@ const forwarder = Object.freeze({
|
|||||||
// e.g. key: "forwarder.DuckDuckGo.local.settings",
|
// e.g. key: "forwarder.DuckDuckGo.local.settings",
|
||||||
key: "duckDuckGoForwarder",
|
key: "duckDuckGoForwarder",
|
||||||
target: "object",
|
target: "object",
|
||||||
format: "classified",
|
format: "secret-state",
|
||||||
classifier: new PrivateClassifier<DuckDuckGoSettings>(),
|
classifier: new PrivateClassifier<DuckDuckGoSettings>(),
|
||||||
state: GENERATOR_DISK,
|
state: GENERATOR_DISK,
|
||||||
|
initial: defaultSettings,
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
@ -123,9 +123,10 @@ const forwarder = Object.freeze({
|
|||||||
// e.g. key: "forwarder.Fastmail.local.settings"
|
// e.g. key: "forwarder.Fastmail.local.settings"
|
||||||
key: "fastmailForwarder",
|
key: "fastmailForwarder",
|
||||||
target: "object",
|
target: "object",
|
||||||
format: "classified",
|
format: "secret-state",
|
||||||
classifier: new PrivateClassifier<FastmailSettings>(),
|
classifier: new PrivateClassifier<FastmailSettings>(),
|
||||||
state: GENERATOR_DISK,
|
state: GENERATOR_DISK,
|
||||||
|
initial: defaultSettings,
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
@ -59,9 +59,10 @@ const forwarder = Object.freeze({
|
|||||||
// e.g. key: "forwarder.Firefox.local.settings",
|
// e.g. key: "forwarder.Firefox.local.settings",
|
||||||
key: "firefoxRelayForwarder",
|
key: "firefoxRelayForwarder",
|
||||||
target: "object",
|
target: "object",
|
||||||
format: "classified",
|
format: "secret-state",
|
||||||
classifier: new PrivateClassifier<FirefoxRelaySettings>(),
|
classifier: new PrivateClassifier<FirefoxRelaySettings>(),
|
||||||
state: GENERATOR_DISK,
|
state: GENERATOR_DISK,
|
||||||
|
initial: defaultSettings,
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
@ -62,9 +62,10 @@ const forwarder = Object.freeze({
|
|||||||
// e.g. key: "forwarder.ForwardEmail.local.settings",
|
// e.g. key: "forwarder.ForwardEmail.local.settings",
|
||||||
key: "forwardEmailForwarder",
|
key: "forwardEmailForwarder",
|
||||||
target: "object",
|
target: "object",
|
||||||
format: "classified",
|
format: "secret-state",
|
||||||
classifier: new PrivateClassifier<ForwardEmailSettings>(),
|
classifier: new PrivateClassifier<ForwardEmailSettings>(),
|
||||||
state: GENERATOR_DISK,
|
state: GENERATOR_DISK,
|
||||||
|
initial: defaultSettings,
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
@ -27,6 +27,7 @@ export type SimpleLoginConfiguration = ForwarderConfiguration<SimpleLoginSetting
|
|||||||
const defaultSettings = Object.freeze({
|
const defaultSettings = Object.freeze({
|
||||||
token: "",
|
token: "",
|
||||||
domain: "",
|
domain: "",
|
||||||
|
baseUrl: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// supported RPC calls
|
// supported RPC calls
|
||||||
@ -64,9 +65,10 @@ const forwarder = Object.freeze({
|
|||||||
// e.g. key: "forwarder.SimpleLogin.local.settings",
|
// e.g. key: "forwarder.SimpleLogin.local.settings",
|
||||||
key: "simpleLoginForwarder",
|
key: "simpleLoginForwarder",
|
||||||
target: "object",
|
target: "object",
|
||||||
format: "classified",
|
format: "secret-state",
|
||||||
classifier: new PrivateClassifier<SimpleLoginSettings>(),
|
classifier: new PrivateClassifier<SimpleLoginSettings>(),
|
||||||
state: GENERATOR_DISK,
|
state: GENERATOR_DISK,
|
||||||
|
initial: defaultSettings,
|
||||||
options: {
|
options: {
|
||||||
deserializer: (value) => value,
|
deserializer: (value) => value,
|
||||||
clearOn: ["logout"],
|
clearOn: ["logout"],
|
||||||
|
@ -5,13 +5,12 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
|
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||||
|
import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction";
|
||||||
import { StateConstraints } from "@bitwarden/common/tools/types";
|
import { StateConstraints } from "@bitwarden/common/tools/types";
|
||||||
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FakeStateProvider,
|
FakeStateProvider,
|
||||||
@ -175,9 +174,8 @@ const i18nService = mock<I18nService>();
|
|||||||
|
|
||||||
const apiService = mock<ApiService>();
|
const apiService = mock<ApiService>();
|
||||||
|
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptor = mock<UserEncryptor>();
|
||||||
|
const encryptorProvider = mock<LegacyEncryptorProvider>();
|
||||||
const keyService = mock<KeyService>();
|
|
||||||
|
|
||||||
describe("CredentialGeneratorService", () => {
|
describe("CredentialGeneratorService", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -185,8 +183,8 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable());
|
policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable());
|
||||||
i18nService.t.mockImplementation((key) => key);
|
i18nService.t.mockImplementation((key) => key);
|
||||||
apiService.fetch.mockImplementation(() => Promise.resolve(mock<Response>()));
|
apiService.fetch.mockImplementation(() => Promise.resolve(mock<Response>()));
|
||||||
const keyAvailable = new BehaviorSubject({} as UserKey);
|
const encryptor$ = new BehaviorSubject({ userId: SomeUser, encryptor });
|
||||||
keyService.userKey$.mockReturnValue(keyAvailable);
|
encryptorProvider.userEncryptor$.mockReturnValue(encryptor$);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,8 +198,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
@ -222,8 +219,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
@ -248,8 +244,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
||||||
@ -277,8 +272,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const website$ = new BehaviorSubject("some website");
|
const website$ = new BehaviorSubject("some website");
|
||||||
@ -299,8 +293,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const website$ = new BehaviorSubject("some website");
|
const website$ = new BehaviorSubject("some website");
|
||||||
@ -325,8 +318,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const website$ = new BehaviorSubject("some website");
|
const website$ = new BehaviorSubject("some website");
|
||||||
@ -352,8 +344,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
@ -373,8 +364,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -398,8 +388,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser);
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
@ -424,8 +413,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser);
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
@ -451,8 +439,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<void>();
|
||||||
@ -494,8 +481,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<void>();
|
||||||
@ -521,8 +507,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<void>();
|
||||||
@ -553,8 +538,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -575,8 +559,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -596,8 +579,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -618,8 +600,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -644,8 +625,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -662,8 +642,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -679,8 +658,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -697,8 +675,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -720,8 +697,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -746,8 +722,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const results: any = [];
|
const results: any = [];
|
||||||
@ -784,8 +759,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
@ -806,8 +780,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -837,8 +810,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -864,8 +836,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -891,8 +862,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -924,8 +894,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -943,8 +912,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -964,8 +932,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -990,8 +957,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const results: any = [];
|
const results: any = [];
|
||||||
@ -1016,8 +982,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
@ -1038,8 +1003,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1066,8 +1030,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1093,8 +1056,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1120,8 +1082,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1153,8 +1114,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
const subject = await generator.settings(SomeConfiguration, { singleUserId$ });
|
||||||
@ -1179,8 +1139,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1206,8 +1165,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
@ -1224,8 +1182,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
const userId$ = new BehaviorSubject(SomeUser).asObservable();
|
||||||
@ -1244,8 +1201,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1274,8 +1230,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1305,8 +1260,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
@ -1332,8 +1286,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
policyService,
|
policyService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
encryptService,
|
encryptorProvider,
|
||||||
keyService,
|
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
const userId = new BehaviorSubject(SomeUser);
|
||||||
|
@ -11,11 +11,11 @@ import {
|
|||||||
ignoreElements,
|
ignoreElements,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
|
ReplaySubject,
|
||||||
share,
|
share,
|
||||||
skipUntil,
|
skipUntil,
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
takeWhile,
|
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { Simplify } from "type-fest";
|
import { Simplify } from "type-fest";
|
||||||
@ -24,24 +24,19 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||||
import {
|
import {
|
||||||
OnDependency,
|
OnDependency,
|
||||||
SingleUserDependency,
|
SingleUserDependency,
|
||||||
UserBound,
|
|
||||||
UserDependency,
|
UserDependency,
|
||||||
} from "@bitwarden/common/tools/dependencies";
|
} from "@bitwarden/common/tools/dependencies";
|
||||||
import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration";
|
import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration";
|
||||||
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { anyComplete } from "@bitwarden/common/tools/rx";
|
import { anyComplete } from "@bitwarden/common/tools/rx";
|
||||||
import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer";
|
|
||||||
import { UserEncryptor } from "@bitwarden/common/tools/state/user-encryptor.abstraction";
|
|
||||||
import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor";
|
|
||||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { Randomizer } from "../abstractions";
|
import { Randomizer } from "../abstractions";
|
||||||
import {
|
import {
|
||||||
@ -97,8 +92,7 @@ export class CredentialGeneratorService {
|
|||||||
private readonly policyService: PolicyService,
|
private readonly policyService: PolicyService,
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly i18nService: I18nService,
|
private readonly i18nService: I18nService,
|
||||||
private readonly encryptService: EncryptService,
|
private readonly encryptorProvider: LegacyEncryptorProvider,
|
||||||
private readonly keyService: KeyService,
|
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -273,21 +267,6 @@ export class CredentialGeneratorService {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private encryptor$(userId: UserId) {
|
|
||||||
const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE);
|
|
||||||
const encryptor$ = this.keyService.userKey$(userId).pipe(
|
|
||||||
// complete when the account locks
|
|
||||||
takeWhile((key) => !!key),
|
|
||||||
map((key) => {
|
|
||||||
const encryptor = new UserKeyEncryptor(userId, this.encryptService, key, packer);
|
|
||||||
|
|
||||||
return { userId, encryptor } satisfies UserBound<"encryptor", UserEncryptor>;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return encryptor$;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the settings for the provided configuration
|
/** Get the settings for the provided configuration
|
||||||
* @param configuration determines which generator's settings are loaded
|
* @param configuration determines which generator's settings are loaded
|
||||||
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
* @param dependencies.userId$ identifies the user to which the settings are bound.
|
||||||
@ -307,10 +286,15 @@ export class CredentialGeneratorService {
|
|||||||
filter((userId) => !!userId),
|
filter((userId) => !!userId),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap((userId) => {
|
switchMap((userId) => {
|
||||||
|
const singleUserId$ = new BehaviorSubject(userId);
|
||||||
|
const singleUserEncryptor$ = this.encryptorProvider.userEncryptor$(OPTIONS_FRAME_SIZE, {
|
||||||
|
singleUserId$,
|
||||||
|
});
|
||||||
|
|
||||||
const state$ = new UserStateSubject(
|
const state$ = new UserStateSubject(
|
||||||
configuration.settings.account,
|
configuration.settings.account,
|
||||||
(key) => this.stateProvider.getUser(userId, key),
|
(key) => this.stateProvider.getUser(userId, key),
|
||||||
{ constraints$, singleUserEncryptor$: this.encryptor$(userId) },
|
{ constraints$, singleUserEncryptor$ },
|
||||||
);
|
);
|
||||||
return state$;
|
return state$;
|
||||||
}),
|
}),
|
||||||
@ -333,15 +317,23 @@ export class CredentialGeneratorService {
|
|||||||
async preferences(
|
async preferences(
|
||||||
dependencies: SingleUserDependency,
|
dependencies: SingleUserDependency,
|
||||||
): Promise<UserStateSubject<CredentialPreference>> {
|
): Promise<UserStateSubject<CredentialPreference>> {
|
||||||
const userId = await firstValueFrom(
|
const singleUserId$ = new ReplaySubject<UserId>(1);
|
||||||
dependencies.singleUserId$.pipe(filter((userId) => !!userId)),
|
dependencies.singleUserId$
|
||||||
);
|
.pipe(
|
||||||
|
filter((userId) => !!userId),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
)
|
||||||
|
.subscribe(singleUserId$);
|
||||||
|
const singleUserEncryptor$ = this.encryptorProvider.userEncryptor$(OPTIONS_FRAME_SIZE, {
|
||||||
|
singleUserId$,
|
||||||
|
});
|
||||||
|
const userId = await firstValueFrom(singleUserId$);
|
||||||
|
|
||||||
// FIXME: enforce policy
|
// FIXME: enforce policy
|
||||||
const subject = new UserStateSubject(
|
const subject = new UserStateSubject(
|
||||||
PREFERENCES,
|
PREFERENCES,
|
||||||
(key) => this.stateProvider.getUser(userId, key),
|
(key) => this.stateProvider.getUser(userId, key),
|
||||||
{ singleUserEncryptor$: this.encryptor$(userId) },
|
{ singleUserEncryptor$ },
|
||||||
);
|
);
|
||||||
|
|
||||||
return subject;
|
return subject;
|
||||||
@ -358,16 +350,24 @@ export class CredentialGeneratorService {
|
|||||||
configuration: Readonly<Configuration<Settings, Policy>>,
|
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||||
dependencies: SingleUserDependency,
|
dependencies: SingleUserDependency,
|
||||||
) {
|
) {
|
||||||
const userId = await firstValueFrom(
|
const singleUserId$ = new ReplaySubject<UserId>(1);
|
||||||
dependencies.singleUserId$.pipe(filter((userId) => !!userId)),
|
dependencies.singleUserId$
|
||||||
);
|
.pipe(
|
||||||
|
filter((userId) => !!userId),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
)
|
||||||
|
.subscribe(singleUserId$);
|
||||||
|
const singleUserEncryptor$ = this.encryptorProvider.userEncryptor$(OPTIONS_FRAME_SIZE, {
|
||||||
|
singleUserId$,
|
||||||
|
});
|
||||||
|
const userId = await firstValueFrom(singleUserId$);
|
||||||
|
|
||||||
const constraints$ = this.policy$(configuration, { userId$: dependencies.singleUserId$ });
|
const constraints$ = this.policy$(configuration, { userId$: dependencies.singleUserId$ });
|
||||||
|
|
||||||
const subject = new UserStateSubject(
|
const subject = new UserStateSubject(
|
||||||
configuration.settings.account,
|
configuration.settings.account,
|
||||||
(key) => this.stateProvider.getUser(userId, key),
|
(key) => this.stateProvider.getUser(userId, key),
|
||||||
{ constraints$, singleUserEncryptor$: this.encryptor$(userId) },
|
{ constraints$, singleUserEncryptor$ },
|
||||||
);
|
);
|
||||||
|
|
||||||
return subject;
|
return subject;
|
||||||
|
@ -5,6 +5,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { UserKeyEncryptor } from "@bitwarden/common/tools/cryptography/user-key-encryptor";
|
||||||
import {
|
import {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
IntegrationRequest,
|
IntegrationRequest,
|
||||||
@ -14,7 +15,6 @@ import { BufferedState } from "@bitwarden/common/tools/state/buffered-state";
|
|||||||
import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer";
|
import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer";
|
||||||
import { SecretKeyDefinition } from "@bitwarden/common/tools/state/secret-key-definition";
|
import { SecretKeyDefinition } from "@bitwarden/common/tools/state/secret-key-definition";
|
||||||
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
|
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
|
||||||
import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor";
|
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@ import { filter, map } from "rxjs";
|
|||||||
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
import { UserKeyEncryptor } from "@bitwarden/common/tools/cryptography/user-key-encryptor";
|
||||||
import { BufferedState } from "@bitwarden/common/tools/state/buffered-state";
|
import { BufferedState } from "@bitwarden/common/tools/state/buffered-state";
|
||||||
import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer";
|
import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer";
|
||||||
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
|
import { SecretState } from "@bitwarden/common/tools/state/secret-state";
|
||||||
import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor";
|
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CredentialAlgorithm } from "@bitwarden/generator-core";
|
import { CredentialAlgorithm } from "@bitwarden/generator-core";
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
@ -1,52 +1,18 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||||
import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens";
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
||||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
|
||||||
import {
|
|
||||||
createRandomizer,
|
|
||||||
CredentialGeneratorService,
|
|
||||||
Randomizer,
|
|
||||||
} from "@bitwarden/generator-core";
|
|
||||||
import { KeyService } from "@bitwarden/key-management";
|
|
||||||
|
|
||||||
import { SendFormService } from "./abstractions/send-form.service";
|
import { SendFormService } from "./abstractions/send-form.service";
|
||||||
import { SendFormComponent } from "./components/send-form.component";
|
import { SendFormComponent } from "./components/send-form.component";
|
||||||
import { DefaultSendFormService } from "./services/default-send-form.service";
|
import { DefaultSendFormService } from "./services/default-send-form.service";
|
||||||
|
|
||||||
const RANDOMIZER = new SafeInjectionToken<Randomizer>("Randomizer");
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SendFormComponent],
|
imports: [SendFormComponent, GeneratorServicesModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: SendFormService,
|
provide: SendFormService,
|
||||||
useClass: DefaultSendFormService,
|
useClass: DefaultSendFormService,
|
||||||
},
|
},
|
||||||
safeProvider({
|
|
||||||
provide: RANDOMIZER,
|
|
||||||
useFactory: createRandomizer,
|
|
||||||
deps: [KeyService],
|
|
||||||
}),
|
|
||||||
safeProvider({
|
|
||||||
useClass: CredentialGeneratorService,
|
|
||||||
provide: CredentialGeneratorService,
|
|
||||||
deps: [
|
|
||||||
RANDOMIZER,
|
|
||||||
StateProvider,
|
|
||||||
PolicyService,
|
|
||||||
ApiService,
|
|
||||||
I18nService,
|
|
||||||
EncryptService,
|
|
||||||
KeyService,
|
|
||||||
AccountService,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
exports: [SendFormComponent],
|
exports: [SendFormComponent],
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user