1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build

This commit is contained in:
John Harrington
2025-12-08 08:19:17 -07:00
committed by GitHub
49 changed files with 871 additions and 631 deletions

View File

@@ -186,15 +186,15 @@ export class EditCommand {
return Response.notFound(); return Response.notFound();
} }
let folderView = await folder.decrypt(); const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
let folderView = await folder.decrypt(userKey);
folderView = FolderExport.toView(req, folderView); folderView = FolderExport.toView(req, folderView);
const userKey = await this.keyService.getUserKey(activeUserId);
const encFolder = await this.folderService.encrypt(folderView, userKey); const encFolder = await this.folderService.encrypt(folderView, userKey);
try { try {
const folder = await this.folderApiService.save(encFolder, activeUserId); const folder = await this.folderApiService.save(encFolder, activeUserId);
const updatedFolder = new Folder(folder); const updatedFolder = new Folder(folder);
const decFolder = await updatedFolder.decrypt(); const decFolder = await updatedFolder.decrypt(userKey);
const res = new FolderResponse(decFolder); const res = new FolderResponse(decFolder);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {

View File

@@ -417,10 +417,11 @@ export class GetCommand extends DownloadCommand {
private async getFolder(id: string) { private async getFolder(id: string) {
let decFolder: FolderView = null; let decFolder: FolderView = null;
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
if (Utils.isGuid(id)) { if (Utils.isGuid(id)) {
const folder = await this.folderService.getFromState(id, activeUserId); const folder = await this.folderService.getFromState(id, activeUserId);
if (folder != null) { if (folder != null) {
decFolder = await folder.decrypt(); decFolder = await folder.decrypt(userKey);
} }
} else if (id.trim() !== "") { } else if (id.trim() !== "") {
let folders = await this.folderService.getAllDecryptedFromState(activeUserId); let folders = await this.folderService.getAllDecryptedFromState(activeUserId);

View File

@@ -3,6 +3,8 @@ import * as sdk from "@bitwarden/sdk-internal";
export class CliSdkLoadService extends SdkLoadService { export class CliSdkLoadService extends SdkLoadService {
async load(): Promise<void> { async load(): Promise<void> {
// CLI uses stdout for user interaction / automations so we cannot log info / debug here.
SdkLoadService.logLevel = sdk.LogLevel.Error;
const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
(sdk as any).init(module); (sdk as any).init(module);
} }

View File

@@ -181,12 +181,12 @@ export class CreateCommand {
private async createFolder(req: FolderExport) { private async createFolder(req: FolderExport) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const userKey = await this.keyService.getUserKey(activeUserId); const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey); const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
try { try {
const folderData = await this.folderApiService.save(folder, activeUserId); const folderData = await this.folderApiService.save(folder, activeUserId);
const newFolder = new Folder(folderData); const newFolder = new Folder(folderData);
const decFolder = await newFolder.decrypt(); const decFolder = await newFolder.decrypt(userKey);
const res = new FolderResponse(decFolder); const res = new FolderResponse(decFolder);
return Response.success(res); return Response.success(res);
} catch (e) { } catch (e) {

View File

@@ -1,4 +1,4 @@
<bit-layout> <bit-layout class="!tw-h-full">
<app-side-nav slot="side-nav"> <app-side-nav slot="side-nav">
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo> <bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo>

View File

@@ -0,0 +1,15 @@
/**
* Desktop UI Migration
*
* These are temporary styles during the desktop ui migration.
**/
/**
* This removes any padding applied by the bit-layout to content.
* This should be revisited once the table is migrated, and again once drawers are migrated.
**/
bit-layout {
#main-content {
padding: 0 0 0 0;
}
}

View File

@@ -15,5 +15,6 @@
@import "left-nav.scss"; @import "left-nav.scss";
@import "loading.scss"; @import "loading.scss";
@import "plugins.scss"; @import "plugins.scss";
@import "migration.scss";
@import "../../../../libs/angular/src/scss/icons.scss"; @import "../../../../libs/angular/src/scss/icons.scss";
@import "../../../../libs/components/src/multi-select/scss/bw.theme"; @import "../../../../libs/components/src/multi-select/scss/bw.theme";

View File

@@ -2,7 +2,10 @@
// @ts-strict-ignore // @ts-strict-ignore
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { KeyService } from "@bitwarden/key-management";
import { EncryptionType } from "../src/platform/enums"; import { EncryptionType } from "../src/platform/enums";
import { Utils } from "../src/platform/misc/utils"; import { Utils } from "../src/platform/misc/utils";
@@ -29,6 +32,7 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
export function mockEnc(s: string): MockProxy<EncString> { export function mockEnc(s: string): MockProxy<EncString> {
const mocked = mock<EncString>(); const mocked = mock<EncString>();
mocked.decryptedValue = s;
mocked.decrypt.mockResolvedValue(s); mocked.decrypt.mockResolvedValue(s);
return mocked; return mocked;
@@ -77,4 +81,14 @@ export const mockFromSdk = (stub: any) => {
return `${stub}_fromSdk`; return `${stub}_fromSdk`;
}; };
export const mockContainerService = () => {
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
encryptService.decryptString.mockImplementation(async (encStr, _key) => {
return encStr.decryptedValue;
});
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
return (window as any).bitwardenContainerService;
};
export { trackEmissions, awaitAsync } from "@bitwarden/core-test-utils"; export { trackEmissions, awaitAsync } from "@bitwarden/core-test-utils";

View File

@@ -1,4 +1,4 @@
import { init_sdk } from "@bitwarden/sdk-internal"; import { init_sdk, LogLevel } from "@bitwarden/sdk-internal";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
import type { SdkService } from "./sdk.service"; import type { SdkService } from "./sdk.service";
@@ -10,6 +10,7 @@ export class SdkLoadFailedError extends Error {
} }
export abstract class SdkLoadService { export abstract class SdkLoadService {
protected static logLevel: LogLevel = LogLevel.Info;
private static markAsReady: () => void; private static markAsReady: () => void;
private static markAsFailed: (error: unknown) => void; private static markAsFailed: (error: unknown) => void;
@@ -41,7 +42,7 @@ export abstract class SdkLoadService {
async loadAndInit(): Promise<void> { async loadAndInit(): Promise<void> {
try { try {
await this.load(); await this.load();
init_sdk(); init_sdk(SdkLoadService.logLevel);
SdkLoadService.markAsReady(); SdkLoadService.markAsReady();
} catch (error) { } catch (error) {
SdkLoadService.markAsFailed(error); SdkLoadService.markAsFailed(error);

View File

@@ -73,14 +73,13 @@ export default class Domain {
domain: DomainEncryptableKeys<D>, domain: DomainEncryptableKeys<D>,
viewModel: ViewEncryptableKeys<V>, viewModel: ViewEncryptableKeys<V>,
props: EncryptableKeys<D, V>[], props: EncryptableKeys<D, V>[],
orgId: string | null,
key: SymmetricCryptoKey | null = null, key: SymmetricCryptoKey | null = null,
objectContext: string = "No Domain Context", objectContext: string = "No Domain Context",
): Promise<V> { ): Promise<V> {
for (const prop of props) { for (const prop of props) {
viewModel[prop] = viewModel[prop] =
(await domain[prop]?.decrypt( (await domain[prop]?.decrypt(
orgId, null,
key, key,
`Property: ${prop as string}; ObjectContext: ${objectContext}`, `Property: ${prop as string}; ObjectContext: ${objectContext}`,
)) ?? null; )) ?? null;

View File

@@ -1,6 +1,6 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { mockEnc } from "../../../../../spec"; import { mockContainerService, mockEnc } from "../../../../../spec";
import { SendType } from "../../enums/send-type"; import { SendType } from "../../enums/send-type";
import { SendAccessResponse } from "../response/send-access.response"; import { SendAccessResponse } from "../response/send-access.response";
@@ -23,6 +23,8 @@ describe("SendAccess", () => {
expirationDate: new Date("2022-01-31T12:00:00.000Z"), expirationDate: new Date("2022-01-31T12:00:00.000Z"),
creatorIdentifier: "creatorIdentifier", creatorIdentifier: "creatorIdentifier",
} as SendAccessResponse; } as SendAccessResponse;
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -54,7 +54,7 @@ export class SendAccess extends Domain {
async decrypt(key: SymmetricCryptoKey): Promise<SendAccessView> { async decrypt(key: SymmetricCryptoKey): Promise<SendAccessView> {
const model = new SendAccessView(this); const model = new SendAccessView(this);
await this.decryptObj<SendAccess, SendAccessView>(this, model, ["name"], null, key); await this.decryptObj<SendAccess, SendAccessView>(this, model, ["name"], key);
switch (this.type) { switch (this.type) {
case SendType.File: case SendType.File:

View File

@@ -1,4 +1,4 @@
import { mockEnc } from "../../../../../spec"; import { mockContainerService, mockEnc } from "../../../../../spec";
import { SendFileData } from "../data/send-file.data"; import { SendFileData } from "../data/send-file.data";
import { SendFile } from "./send-file"; import { SendFile } from "./send-file";
@@ -39,6 +39,7 @@ describe("SendFile", () => {
}); });
it("Decrypt", async () => { it("Decrypt", async () => {
mockContainerService();
const sendFile = new SendFile(); const sendFile = new SendFile();
sendFile.id = "id"; sendFile.id = "id";
sendFile.size = "1100"; sendFile.size = "1100";

View File

@@ -38,7 +38,6 @@ export class SendFile extends Domain {
this, this,
new SendFileView(this), new SendFileView(this),
["fileName"], ["fileName"],
null,
key, key,
); );
} }

View File

@@ -1,4 +1,4 @@
import { mockEnc } from "../../../../../spec"; import { mockContainerService, mockEnc } from "../../../../../spec";
import { SendTextData } from "../data/send-text.data"; import { SendTextData } from "../data/send-text.data";
import { SendText } from "./send-text"; import { SendText } from "./send-text";
@@ -11,6 +11,8 @@ describe("SendText", () => {
text: "encText", text: "encText",
hidden: false, hidden: false,
}; };
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -30,13 +30,7 @@ export class SendText extends Domain {
} }
decrypt(key: SymmetricCryptoKey): Promise<SendTextView> { decrypt(key: SymmetricCryptoKey): Promise<SendTextView> {
return this.decryptObj<SendText, SendTextView>( return this.decryptObj<SendText, SendTextView>(this, new SendTextView(this), ["text"], key);
this,
new SendTextView(this),
["text"],
null,
key,
);
} }
static fromJSON(obj: Jsonify<SendText>) { static fromJSON(obj: Jsonify<SendText>) {

View File

@@ -6,7 +6,7 @@ import { emptyGuid, UserId } from "@bitwarden/common/types/guid";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { makeStaticByteArray, mockEnc } from "../../../../../spec"; import { makeStaticByteArray, mockContainerService, mockEnc } from "../../../../../spec";
import { EncryptService } from "../../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "../../../../key-management/crypto/abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../../platform/services/container.service"; import { ContainerService } from "../../../../platform/services/container.service";
@@ -43,6 +43,8 @@ describe("Send", () => {
disabled: false, disabled: false,
hideEmail: true, hideEmail: true,
}; };
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -89,7 +89,7 @@ export class Send extends Domain {
model.key = await encryptService.decryptBytes(this.key, sendKeyEncryptionKey); model.key = await encryptService.decryptBytes(this.key, sendKeyEncryptionKey);
model.cryptoKey = await keyService.makeSendKey(model.key); model.cryptoKey = await keyService.makeSendKey(model.key);
await this.decryptObj<Send, SendView>(this, model, ["name", "notes"], null, model.cryptoKey); await this.decryptObj<Send, SendView>(this, model, ["name", "notes"], model.cryptoKey);
switch (this.type) { switch (this.type) {
case SendType.File: case SendType.File:

View File

@@ -4,12 +4,10 @@ import { mock, MockProxy } from "jest-mock-extended";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management"; import { KeyService } from "@bitwarden/key-management";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; import { makeStaticByteArray, mockContainerService, mockEnc, mockFromJson } from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service";
import { OrgKey, UserKey } from "../../../types/key";
import { AttachmentData } from "../../models/data/attachment.data"; import { AttachmentData } from "../../models/data/attachment.data";
import { Attachment } from "../../models/domain/attachment"; import { Attachment } from "../../models/domain/attachment";
@@ -70,10 +68,9 @@ describe("Attachment", () => {
let encryptService: MockProxy<EncryptService>; let encryptService: MockProxy<EncryptService>;
beforeEach(() => { beforeEach(() => {
keyService = mock<KeyService>(); const containerService = mockContainerService();
encryptService = mock<EncryptService>(); keyService = containerService.keyService as MockProxy<KeyService>;
encryptService = containerService.encryptService as MockProxy<EncryptService>;
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
}); });
it("expected output", async () => { it("expected output", async () => {
@@ -85,14 +82,13 @@ describe("Attachment", () => {
attachment.key = mockEnc("key"); attachment.key = mockEnc("key");
attachment.fileName = mockEnc("fileName"); attachment.fileName = mockEnc("fileName");
const userKey = new SymmetricCryptoKey(makeStaticByteArray(64));
keyService.getUserKey.mockResolvedValue(userKey as UserKey);
encryptService.decryptFileData.mockResolvedValue(makeStaticByteArray(32)); encryptService.decryptFileData.mockResolvedValue(makeStaticByteArray(32));
encryptService.unwrapSymmetricKey.mockResolvedValue( encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(64)), new SymmetricCryptoKey(makeStaticByteArray(64)),
); );
const view = await attachment.decrypt(null); const userKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const view = await attachment.decrypt(userKey);
expect(view).toEqual({ expect(view).toEqual({
id: "id", id: "id",
@@ -116,31 +112,11 @@ describe("Attachment", () => {
it("uses the provided key without depending on KeyService", async () => { it("uses the provided key without depending on KeyService", async () => {
const providedKey = mock<SymmetricCryptoKey>(); const providedKey = mock<SymmetricCryptoKey>();
await attachment.decrypt(null, "", providedKey); await attachment.decrypt(providedKey, "");
expect(keyService.getUserKey).not.toHaveBeenCalled(); expect(keyService.getUserKey).not.toHaveBeenCalled();
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, providedKey); expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, providedKey);
}); });
it("gets an organization key if required", async () => {
const orgKey = mock<OrgKey>();
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await attachment.decrypt("orgId", "", null);
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, orgKey);
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<UserKey>();
keyService.getUserKey.mockResolvedValue(userKey);
await attachment.decrypt(null, "", null);
expect(keyService.getUserKey).toHaveBeenCalled();
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, userKey);
});
}); });
}); });

View File

@@ -1,6 +1,5 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal"; import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
@@ -34,21 +33,19 @@ export class Attachment extends Domain {
} }
async decrypt( async decrypt(
orgId: string | undefined, decryptionKey: SymmetricCryptoKey,
context = "No Cipher Context", context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<AttachmentView> { ): Promise<AttachmentView> {
const view = await this.decryptObj<Attachment, AttachmentView>( const view = await this.decryptObj<Attachment, AttachmentView>(
this, this,
new AttachmentView(this), new AttachmentView(this),
["fileName"], ["fileName"],
orgId ?? null, decryptionKey,
encKey,
"DomainType: Attachment; " + context, "DomainType: Attachment; " + context,
); );
if (this.key != null) { if (this.key != null) {
view.key = await this.decryptAttachmentKey(orgId, encKey); view.key = await this.decryptAttachmentKey(decryptionKey);
view.encryptedKey = this.key; // Keep the encrypted key for the view view.encryptedKey = this.key; // Keep the encrypted key for the view
} }
@@ -56,27 +53,15 @@ export class Attachment extends Domain {
} }
private async decryptAttachmentKey( private async decryptAttachmentKey(
orgId: string | undefined, decryptionKey: SymmetricCryptoKey,
encKey?: SymmetricCryptoKey,
): Promise<SymmetricCryptoKey | undefined> { ): Promise<SymmetricCryptoKey | undefined> {
try { try {
if (this.key == null) { if (this.key == null) {
return undefined; return undefined;
} }
if (encKey == null) {
const key = await this.getKeyForDecryption(orgId);
// If we don't have a key, we can't decrypt
if (key == null) {
return undefined;
}
encKey = key;
}
const encryptService = Utils.getContainerService().getEncryptService(); const encryptService = Utils.getContainerService().getEncryptService();
const decValue = await encryptService.unwrapSymmetricKey(this.key, encKey); const decValue = await encryptService.unwrapSymmetricKey(this.key, decryptionKey);
return decValue; return decValue;
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@@ -85,11 +70,6 @@ export class Attachment extends Domain {
} }
} }
private async getKeyForDecryption(orgId: string | undefined): Promise<OrgKey | UserKey | null> {
const keyService = Utils.getContainerService().getKeyService();
return orgId != null ? await keyService.getOrgKey(orgId) : await keyService.getUserKey();
}
toAttachmentData(): AttachmentData { toAttachmentData(): AttachmentData {
const a = new AttachmentData(); const a = new AttachmentData();
if (this.size != null) { if (this.size != null) {

View File

@@ -1,4 +1,9 @@
import { mockEnc, mockFromJson } from "../../../../spec"; import {
makeSymmetricCryptoKey,
mockContainerService,
mockEnc,
mockFromJson,
} from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { CardData } from "../../../vault/models/data/card.data"; import { CardData } from "../../../vault/models/data/card.data";
import { Card } from "../../models/domain/card"; import { Card } from "../../models/domain/card";
@@ -65,7 +70,10 @@ describe("Card", () => {
card.expYear = mockEnc("expYear"); card.expYear = mockEnc("expYear");
card.code = mockEnc("code"); card.code = mockEnc("code");
const view = await card.decrypt(null); const userKey = makeSymmetricCryptoKey(64);
mockContainerService();
const view = await card.decrypt(userKey);
expect(view).toEqual({ expect(view).toEqual({
_brand: "brand", _brand: "brand",

View File

@@ -31,16 +31,11 @@ export class Card extends Domain {
this.code = conditionalEncString(obj.code); this.code = conditionalEncString(obj.code);
} }
async decrypt( async decrypt(encKey: SymmetricCryptoKey, context = "No Cipher Context"): Promise<CardView> {
orgId: string | undefined,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<CardView> {
return this.decryptObj<Card, CardView>( return this.decryptObj<Card, CardView>(
this, this,
new CardView(), new CardView(),
["cardholderName", "brand", "number", "expMonth", "expYear", "code"], ["cardholderName", "brand", "number", "expMonth", "expYear", "code"],
orgId ?? null,
encKey, encKey,
"DomainType: Card; " + context, "DomainType: Card; " + context,
); );

View File

@@ -2,9 +2,7 @@ import { mock } from "jest-mock-extended";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. import { MockProxy } from "@bitwarden/common/platform/spec/mock-deep";
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
import { import {
CipherType as SdkCipherType, CipherType as SdkCipherType,
UriMatchType, UriMatchType,
@@ -14,11 +12,15 @@ import {
EncString as SdkEncString, EncString as SdkEncString,
} from "@bitwarden/sdk-internal"; } from "@bitwarden/sdk-internal";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import {
makeStaticByteArray,
mockContainerService,
mockEnc,
mockFromJson,
} from "../../../../spec/utils";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../../models/domain/domain-service"; import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { ContainerService } from "../../../platform/services/container.service";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { CipherService } from "../../abstractions/cipher.service"; import { CipherService } from "../../abstractions/cipher.service";
@@ -39,7 +41,16 @@ import { IdentityView } from "../../models/view/identity.view";
import { LoginView } from "../../models/view/login.view"; import { LoginView } from "../../models/view/login.view";
import { CipherPermissionsApi } from "../api/cipher-permissions.api"; import { CipherPermissionsApi } from "../api/cipher-permissions.api";
const mockSymmetricKey = new SymmetricCryptoKey(makeStaticByteArray(64));
describe("Cipher DTO", () => { describe("Cipher DTO", () => {
let encryptService: MockProxy<EncryptService>;
beforeEach(() => {
const containerService = mockContainerService();
encryptService = containerService.encryptService;
});
it("Convert from empty CipherData", () => { it("Convert from empty CipherData", () => {
const data = new CipherData(); const data = new CipherData();
const cipher = new Cipher(data); const cipher = new Cipher(data);
@@ -95,13 +106,12 @@ describe("Cipher DTO", () => {
login.decrypt.mockResolvedValue(loginView); login.decrypt.mockResolvedValue(loginView);
cipher.login = login; cipher.login = login;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>(); const cipherService = mock<CipherService>();
encryptService.unwrapSymmetricKey.mockRejectedValue(new Error("Failed to unwrap key")); encryptService.unwrapSymmetricKey.mockRejectedValue(new Error("Failed to unwrap key"));
cipherService.getKeyForCipherKeyDecryption.mockResolvedValue(
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); new SymmetricCryptoKey(makeStaticByteArray(64)),
);
const cipherView = await cipher.decrypt( const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
@@ -317,19 +327,11 @@ describe("Cipher DTO", () => {
login.decrypt.mockResolvedValue(loginView); login.decrypt.mockResolvedValue(loginView);
cipher.login = login; cipher.login = login;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.unwrapSymmetricKey.mockResolvedValue( encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(64)), new SymmetricCryptoKey(makeStaticByteArray(64)),
); );
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt(mockSymmetricKey);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",
@@ -445,19 +447,11 @@ describe("Cipher DTO", () => {
cipher.permissions = new CipherPermissionsApi(); cipher.permissions = new CipherPermissionsApi();
cipher.archivedDate = undefined; cipher.archivedDate = undefined;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.unwrapSymmetricKey.mockResolvedValue( encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(64)), new SymmetricCryptoKey(makeStaticByteArray(64)),
); );
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt(mockSymmetricKey);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",
@@ -591,19 +585,11 @@ describe("Cipher DTO", () => {
card.decrypt.mockResolvedValue(cardView); card.decrypt.mockResolvedValue(cardView);
cipher.card = card; cipher.card = card;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.unwrapSymmetricKey.mockResolvedValue( encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(64)), new SymmetricCryptoKey(makeStaticByteArray(64)),
); );
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt(mockSymmetricKey);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",
@@ -761,19 +747,11 @@ describe("Cipher DTO", () => {
identity.decrypt.mockResolvedValue(identityView); identity.decrypt.mockResolvedValue(identityView);
cipher.identity = identity; cipher.identity = identity;
const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>();
const cipherService = mock<CipherService>();
encryptService.unwrapSymmetricKey.mockResolvedValue( encryptService.unwrapSymmetricKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(64)), new SymmetricCryptoKey(makeStaticByteArray(64)),
); );
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt(mockSymmetricKey);
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
);
expect(cipherView).toMatchObject({ expect(cipherView).toMatchObject({
id: "id", id: "id",

View File

@@ -1,5 +1,6 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { assertNonNullish } from "@bitwarden/common/auth/utils";
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
@@ -123,19 +124,22 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
} }
} }
// We are passing the organizationId into the EncString.decrypt() method here, but because the encKey will always be async decrypt(userKeyOrOrgKey: SymmetricCryptoKey): Promise<CipherView> {
// present and so the organizationId will not be used. assertNonNullish(userKeyOrOrgKey, "userKeyOrOrgKey", "Cipher decryption");
// We will refactor the EncString.decrypt() in https://bitwarden.atlassian.net/browse/PM-3762 to remove the dependency on the organizationId.
async decrypt(encKey: SymmetricCryptoKey): Promise<CipherView> {
const model = new CipherView(this); const model = new CipherView(this);
let bypassValidation = true; let bypassValidation = true;
// By default, the user/organization key is used for decryption
let cipherDecryptionKey = userKeyOrOrgKey;
// If there is a cipher key present, unwrap it and use it for decryption
if (this.key != null) { if (this.key != null) {
const encryptService = Utils.getContainerService().getEncryptService(); const encryptService = Utils.getContainerService().getEncryptService();
try { try {
const cipherKey = await encryptService.unwrapSymmetricKey(this.key, encKey); const cipherKey = await encryptService.unwrapSymmetricKey(this.key, userKeyOrOrgKey);
encKey = cipherKey; cipherDecryptionKey = cipherKey;
bypassValidation = false; bypassValidation = false;
} catch { } catch {
model.name = "[error: cannot decrypt]"; model.name = "[error: cannot decrypt]";
@@ -144,22 +148,15 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
} }
} }
await this.decryptObj<Cipher, CipherView>( await this.decryptObj<Cipher, CipherView>(this, model, ["name", "notes"], cipherDecryptionKey);
this,
model,
["name", "notes"],
this.organizationId ?? null,
encKey,
);
switch (this.type) { switch (this.type) {
case CipherType.Login: case CipherType.Login:
if (this.login != null) { if (this.login != null) {
model.login = await this.login.decrypt( model.login = await this.login.decrypt(
this.organizationId,
bypassValidation, bypassValidation,
userKeyOrOrgKey,
`Cipher Id: ${this.id}`, `Cipher Id: ${this.id}`,
encKey,
); );
} }
break; break;
@@ -170,29 +167,17 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
break; break;
case CipherType.Card: case CipherType.Card:
if (this.card != null) { if (this.card != null) {
model.card = await this.card.decrypt( model.card = await this.card.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
);
} }
break; break;
case CipherType.Identity: case CipherType.Identity:
if (this.identity != null) { if (this.identity != null) {
model.identity = await this.identity.decrypt( model.identity = await this.identity.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
);
} }
break; break;
case CipherType.SshKey: case CipherType.SshKey:
if (this.sshKey != null) { if (this.sshKey != null) {
model.sshKey = await this.sshKey.decrypt( model.sshKey = await this.sshKey.decrypt(userKeyOrOrgKey, `Cipher Id: ${this.id}`);
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
);
} }
break; break;
default: default:
@@ -203,9 +188,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
const attachments: AttachmentView[] = []; const attachments: AttachmentView[] = [];
for (const attachment of this.attachments) { for (const attachment of this.attachments) {
const decryptedAttachment = await attachment.decrypt( const decryptedAttachment = await attachment.decrypt(
this.organizationId, userKeyOrOrgKey,
`Cipher Id: ${this.id}`, `Cipher Id: ${this.id}`,
encKey,
); );
attachments.push(decryptedAttachment); attachments.push(decryptedAttachment);
} }
@@ -215,7 +199,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.fields != null && this.fields.length > 0) { if (this.fields != null && this.fields.length > 0) {
const fields: FieldView[] = []; const fields: FieldView[] = [];
for (const field of this.fields) { for (const field of this.fields) {
const decryptedField = await field.decrypt(this.organizationId, encKey); const decryptedField = await field.decrypt(userKeyOrOrgKey);
fields.push(decryptedField); fields.push(decryptedField);
} }
model.fields = fields; model.fields = fields;
@@ -224,7 +208,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.passwordHistory != null && this.passwordHistory.length > 0) { if (this.passwordHistory != null && this.passwordHistory.length > 0) {
const passwordHistory: PasswordHistoryView[] = []; const passwordHistory: PasswordHistoryView[] = [];
for (const ph of this.passwordHistory) { for (const ph of this.passwordHistory) {
const decryptedPh = await ph.decrypt(this.organizationId, encKey); const decryptedPh = await ph.decrypt(userKeyOrOrgKey);
passwordHistory.push(decryptedPh); passwordHistory.push(decryptedPh);
} }
model.passwordHistory = passwordHistory; model.passwordHistory = passwordHistory;

View File

@@ -1,4 +1,4 @@
import { mockEnc } from "../../../../spec"; import { makeSymmetricCryptoKey, mockContainerService, mockEnc } from "../../../../spec";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
import { EncryptionType } from "../../../platform/enums"; import { EncryptionType } from "../../../platform/enums";
import { Fido2CredentialData } from "../data/fido2-credential.data"; import { Fido2CredentialData } from "../data/fido2-credential.data";
@@ -103,7 +103,10 @@ describe("Fido2Credential", () => {
credential.discoverable = mockEnc("true"); credential.discoverable = mockEnc("true");
credential.creationDate = mockDate; credential.creationDate = mockDate;
const credentialView = await credential.decrypt(null); mockContainerService();
const cipherKey = makeSymmetricCryptoKey(64);
const credentialView = await credential.decrypt(cipherKey);
expect(credentialView).toEqual({ expect(credentialView).toEqual({
credentialId: "credentialId", credentialId: "credentialId",

View File

@@ -46,10 +46,7 @@ export class Fido2Credential extends Domain {
this.creationDate = new Date(obj.creationDate); this.creationDate = new Date(obj.creationDate);
} }
async decrypt( async decrypt(decryptionKey: SymmetricCryptoKey): Promise<Fido2CredentialView> {
orgId: string | undefined,
encKey?: SymmetricCryptoKey,
): Promise<Fido2CredentialView> {
const view = await this.decryptObj<Fido2Credential, Fido2CredentialView>( const view = await this.decryptObj<Fido2Credential, Fido2CredentialView>(
this, this,
new Fido2CredentialView(), new Fido2CredentialView(),
@@ -65,8 +62,7 @@ export class Fido2Credential extends Domain {
"rpName", "rpName",
"userDisplayName", "userDisplayName",
], ],
orgId ?? null, decryptionKey,
encKey,
); );
const { counter } = await this.decryptObj< const { counter } = await this.decryptObj<
@@ -74,7 +70,7 @@ export class Fido2Credential extends Domain {
{ {
counter: string; counter: string;
} }
>(this, { counter: "" }, ["counter"], orgId ?? null, encKey); >(this, { counter: "" }, ["counter"], decryptionKey);
// Counter will end up as NaN if this fails // Counter will end up as NaN if this fails
view.counter = parseInt(counter); view.counter = parseInt(counter);
@@ -82,8 +78,7 @@ export class Fido2Credential extends Domain {
this, this,
{ discoverable: "" }, { discoverable: "" },
["discoverable"], ["discoverable"],
orgId ?? null, decryptionKey,
encKey,
); );
view.discoverable = discoverable === "true"; view.discoverable = discoverable === "true";
view.creationDate = this.creationDate; view.creationDate = this.creationDate;

View File

@@ -6,7 +6,7 @@ import {
IdentityLinkedIdType, IdentityLinkedIdType,
} from "@bitwarden/sdk-internal"; } from "@bitwarden/sdk-internal";
import { mockEnc, mockFromJson } from "../../../../spec"; import { mockContainerService, mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { CardLinkedId, IdentityLinkedId, LoginLinkedId } from "../../enums"; import { CardLinkedId, IdentityLinkedId, LoginLinkedId } from "../../enums";
import { FieldData } from "../../models/data/field.data"; import { FieldData } from "../../models/data/field.data";
@@ -22,6 +22,7 @@ describe("Field", () => {
value: "encValue", value: "encValue",
linkedId: null, linkedId: null,
}; };
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -33,14 +33,8 @@ export class Field extends Domain {
this.value = conditionalEncString(obj.value); this.value = conditionalEncString(obj.value);
} }
decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise<FieldView> { decrypt(encKey: SymmetricCryptoKey): Promise<FieldView> {
return this.decryptObj<Field, FieldView>( return this.decryptObj<Field, FieldView>(this, new FieldView(this), ["name", "value"], encKey);
this,
new FieldView(this),
["name", "value"],
orgId ?? null,
encKey,
);
} }
toFieldData(): FieldData { toFieldData(): FieldData {

View File

@@ -1,6 +1,12 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { makeEncString, makeSymmetricCryptoKey, mockEnc, mockFromJson } from "../../../../spec"; import {
makeEncString,
makeSymmetricCryptoKey,
mockContainerService,
mockEnc,
mockFromJson,
} from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { FolderData } from "../../models/data/folder.data"; import { FolderData } from "../../models/data/folder.data";
@@ -15,6 +21,7 @@ describe("Folder", () => {
name: "encName", name: "encName",
revisionDate: "2022-01-31T12:00:00.000Z", revisionDate: "2022-01-31T12:00:00.000Z",
}; };
mockContainerService();
}); });
it("Convert", () => { it("Convert", () => {
@@ -33,7 +40,7 @@ describe("Folder", () => {
folder.name = mockEnc("encName"); folder.name = mockEnc("encName");
folder.revisionDate = new Date("2022-01-31T12:00:00.000Z"); folder.revisionDate = new Date("2022-01-31T12:00:00.000Z");
const view = await folder.decrypt(); const view = await folder.decrypt(null);
expect(view).toEqual({ expect(view).toEqual({
id: "id", id: "id",

View File

@@ -39,8 +39,8 @@ export class Folder extends Domain {
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
} }
decrypt(): Promise<FolderView> { decrypt(key: SymmetricCryptoKey): Promise<FolderView> {
return this.decryptObj<Folder, FolderView>(this, new FolderView(this), ["name"], null); return this.decryptObj<Folder, FolderView>(this, new FolderView(this), ["name"], key);
} }
async decryptWithKey( async decryptWithKey(

View File

@@ -1,4 +1,4 @@
import { mockEnc, mockFromJson } from "../../../../spec"; import { mockContainerService, mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { IdentityData } from "../../models/data/identity.data"; import { IdentityData } from "../../models/data/identity.data";
import { Identity } from "../../models/domain/identity"; import { Identity } from "../../models/domain/identity";
@@ -27,6 +27,8 @@ describe("Identity", () => {
passportNumber: "encpassportNumber", passportNumber: "encpassportNumber",
licenseNumber: "enclicenseNumber", licenseNumber: "enclicenseNumber",
}; };
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -56,9 +56,8 @@ export class Identity extends Domain {
} }
decrypt( decrypt(
orgId: string | undefined, encKey: SymmetricCryptoKey,
context: string = "No Cipher Context", context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<IdentityView> { ): Promise<IdentityView> {
return this.decryptObj<Identity, IdentityView>( return this.decryptObj<Identity, IdentityView>(
this, this,
@@ -83,7 +82,6 @@ export class Identity extends Domain {
"passportNumber", "passportNumber",
"licenseNumber", "licenseNumber",
], ],
orgId ?? null,
encKey, encKey,
"DomainType: Identity; " + context, "DomainType: Identity; " + context,
); );

View File

@@ -1,9 +1,14 @@
import { MockProxy, mock } from "jest-mock-extended"; import { MockProxy } from "jest-mock-extended";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { UriMatchType } from "@bitwarden/sdk-internal"; import { UriMatchType } from "@bitwarden/sdk-internal";
import { mockEnc, mockFromJson } from "../../../../spec"; import {
makeSymmetricCryptoKey,
mockContainerService,
mockEnc,
mockFromJson,
} from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../../key-management/crypto/models/enc-string"; import { EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../../models/domain/domain-service"; import { UriMatchStrategy } from "../../../models/domain/domain-service";
@@ -14,6 +19,7 @@ import { LoginUri } from "./login-uri";
describe("LoginUri", () => { describe("LoginUri", () => {
let data: LoginUriData; let data: LoginUriData;
let encryptService: MockProxy<EncryptService>;
beforeEach(() => { beforeEach(() => {
data = { data = {
@@ -21,6 +27,9 @@ describe("LoginUri", () => {
uriChecksum: "encUriChecksum", uriChecksum: "encUriChecksum",
match: UriMatchStrategy.Domain, match: UriMatchStrategy.Domain,
}; };
const containerService = mockContainerService();
encryptService = containerService.getEncryptService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {
@@ -83,22 +92,13 @@ describe("LoginUri", () => {
}); });
describe("validateChecksum", () => { describe("validateChecksum", () => {
let encryptService: MockProxy<EncryptService>;
beforeEach(() => {
encryptService = mock();
global.bitwardenContainerService = {
getEncryptService: () => encryptService,
getKeyService: () => null,
};
});
it("returns true if checksums match", async () => { it("returns true if checksums match", async () => {
const loginUri = new LoginUri(); const loginUri = new LoginUri();
loginUri.uriChecksum = mockEnc("checksum"); loginUri.uriChecksum = mockEnc("checksum");
encryptService.hash.mockResolvedValue("checksum"); encryptService.hash.mockResolvedValue("checksum");
const actual = await loginUri.validateChecksum("uri", undefined, undefined); const key = makeSymmetricCryptoKey(64);
const actual = await loginUri.validateChecksum("uri", key);
expect(actual).toBe(true); expect(actual).toBe(true);
expect(encryptService.hash).toHaveBeenCalledWith("uri", "sha256"); expect(encryptService.hash).toHaveBeenCalledWith("uri", "sha256");
@@ -109,7 +109,7 @@ describe("LoginUri", () => {
loginUri.uriChecksum = mockEnc("checksum"); loginUri.uriChecksum = mockEnc("checksum");
encryptService.hash.mockResolvedValue("incorrect checksum"); encryptService.hash.mockResolvedValue("incorrect checksum");
const actual = await loginUri.validateChecksum("uri", undefined, undefined); const actual = await loginUri.validateChecksum("uri", undefined);
expect(actual).toBe(false); expect(actual).toBe(false);
}); });

View File

@@ -31,29 +31,27 @@ export class LoginUri extends Domain {
} }
decrypt( decrypt(
orgId: string | undefined, encKey: SymmetricCryptoKey,
context: string = "No Cipher Context", context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<LoginUriView> { ): Promise<LoginUriView> {
return this.decryptObj<LoginUri, LoginUriView>( return this.decryptObj<LoginUri, LoginUriView>(
this, this,
new LoginUriView(this), new LoginUriView(this),
["uri"], ["uri"],
orgId ?? null,
encKey, encKey,
context, context,
); );
} }
async validateChecksum(clearTextUri: string, orgId?: string, encKey?: SymmetricCryptoKey) { async validateChecksum(clearTextUri: string, encKey: SymmetricCryptoKey) {
if (this.uriChecksum == null) { if (this.uriChecksum == null) {
return false; return false;
} }
const keyService = Utils.getContainerService().getEncryptService(); const encryptService = Utils.getContainerService().getEncryptService();
const localChecksum = await keyService.hash(clearTextUri, "sha256"); const localChecksum = await encryptService.hash(clearTextUri, "sha256");
const remoteChecksum = await this.uriChecksum.decrypt(orgId ?? null, encKey); const remoteChecksum = await encryptService.decryptString(this.uriChecksum, encKey);
return remoteChecksum === localChecksum; return remoteChecksum === localChecksum;
} }

View File

@@ -1,6 +1,6 @@
import { MockProxy, mock } from "jest-mock-extended"; import { MockProxy, mock } from "jest-mock-extended";
import { mockEnc, mockFromJson } from "../../../../spec"; import { mockContainerService, mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategy } from "../../../models/domain/domain-service"; import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { LoginData } from "../../models/data/login.data"; import { LoginData } from "../../models/data/login.data";
@@ -14,6 +14,10 @@ import { Fido2CredentialView } from "../view/fido2-credential.view";
import { Fido2Credential } from "./fido2-credential"; import { Fido2Credential } from "./fido2-credential";
describe("Login DTO", () => { describe("Login DTO", () => {
beforeEach(() => {
mockContainerService();
});
it("Convert from empty LoginData", () => { it("Convert from empty LoginData", () => {
const data = new LoginData(); const data = new LoginData();
const login = new Login(data); const login = new Login(data);
@@ -107,7 +111,7 @@ describe("Login DTO", () => {
loginUri.validateChecksum.mockResolvedValue(true); loginUri.validateChecksum.mockResolvedValue(true);
login.uris = [loginUri]; login.uris = [loginUri];
const loginView = await login.decrypt(null, true); const loginView = await login.decrypt(true, null);
expect(loginView).toEqual(expectedView); expect(loginView).toEqual(expectedView);
}); });
@@ -119,7 +123,7 @@ describe("Login DTO", () => {
.mockResolvedValueOnce(true); .mockResolvedValueOnce(true);
login.uris = [loginUri, loginUri, loginUri]; login.uris = [loginUri, loginUri, loginUri];
const loginView = await login.decrypt(null, false); const loginView = await login.decrypt(false, null);
expect(loginView).toEqual(expectedView); expect(loginView).toEqual(expectedView);
}); });
}); });

View File

@@ -44,16 +44,14 @@ export class Login extends Domain {
} }
async decrypt( async decrypt(
orgId: string | undefined,
bypassValidation: boolean, bypassValidation: boolean,
encKey: SymmetricCryptoKey,
context: string = "No Cipher Context", context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<LoginView> { ): Promise<LoginView> {
const view = await this.decryptObj<Login, LoginView>( const view = await this.decryptObj<Login, LoginView>(
this, this,
new LoginView(this), new LoginView(this),
["username", "password", "totp"], ["username", "password", "totp"],
orgId ?? null,
encKey, encKey,
`DomainType: Login; ${context}`, `DomainType: Login; ${context}`,
); );
@@ -66,7 +64,7 @@ export class Login extends Domain {
continue; continue;
} }
const uri = await this.uris[i].decrypt(orgId, context, encKey); const uri = await this.uris[i].decrypt(encKey, context);
const uriString = uri.uri; const uriString = uri.uri;
if (uriString == null) { if (uriString == null) {
@@ -79,7 +77,7 @@ export class Login extends Domain {
// So we bypass the validation if there's no cipher.key or proceed with the validation and // So we bypass the validation if there's no cipher.key or proceed with the validation and
// Skip the value if it's been tampered with. // Skip the value if it's been tampered with.
const isValidUri = const isValidUri =
bypassValidation || (await this.uris[i].validateChecksum(uriString, orgId, encKey)); bypassValidation || (await this.uris[i].validateChecksum(uriString, encKey));
if (isValidUri) { if (isValidUri) {
view.uris.push(uri); view.uris.push(uri);
@@ -89,7 +87,7 @@ export class Login extends Domain {
if (this.fido2Credentials != null) { if (this.fido2Credentials != null) {
view.fido2Credentials = await Promise.all( view.fido2Credentials = await Promise.all(
this.fido2Credentials.map((key) => key.decrypt(orgId, encKey)), this.fido2Credentials.map((key) => key.decrypt(encKey)),
); );
} }

View File

@@ -1,4 +1,4 @@
import { mockEnc, mockFromJson } from "../../../../spec"; import { mockContainerService, mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string"; import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { PasswordHistoryData } from "../../models/data/password-history.data"; import { PasswordHistoryData } from "../../models/data/password-history.data";
import { Password } from "../../models/domain/password"; import { Password } from "../../models/domain/password";
@@ -11,6 +11,7 @@ describe("Password", () => {
password: "encPassword", password: "encPassword",
lastUsedDate: "2022-01-31T12:00:00.000Z", lastUsedDate: "2022-01-31T12:00:00.000Z",
}; };
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -22,12 +22,11 @@ export class Password extends Domain {
this.lastUsedDate = new Date(obj.lastUsedDate); this.lastUsedDate = new Date(obj.lastUsedDate);
} }
decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise<PasswordHistoryView> { decrypt(encKey: SymmetricCryptoKey): Promise<PasswordHistoryView> {
return this.decryptObj<Password, PasswordHistoryView>( return this.decryptObj<Password, PasswordHistoryView>(
this, this,
new PasswordHistoryView(this), new PasswordHistoryView(this),
["password"], ["password"],
orgId ?? null,
encKey, encKey,
"DomainType: PasswordHistory", "DomainType: PasswordHistory",
); );

View File

@@ -1,3 +1,4 @@
import { mockContainerService } from "../../../../spec";
import { SecureNoteType } from "../../enums"; import { SecureNoteType } from "../../enums";
import { SecureNoteData } from "../data/secure-note.data"; import { SecureNoteData } from "../data/secure-note.data";
@@ -10,6 +11,8 @@ describe("SecureNote", () => {
data = { data = {
type: SecureNoteType.Generic, type: SecureNoteType.Generic,
}; };
mockContainerService();
}); });
it("Convert from empty", () => { it("Convert from empty", () => {

View File

@@ -1,7 +1,7 @@
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { EncString as SdkEncString, SshKey as SdkSshKey } from "@bitwarden/sdk-internal"; import { EncString as SdkEncString, SshKey as SdkSshKey } from "@bitwarden/sdk-internal";
import { mockEnc } from "../../../../spec"; import { mockContainerService, mockEnc } from "../../../../spec";
import { SshKeyApi } from "../api/ssh-key.api"; import { SshKeyApi } from "../api/ssh-key.api";
import { SshKeyData } from "../data/ssh-key.data"; import { SshKeyData } from "../data/ssh-key.data";
@@ -18,6 +18,8 @@ describe("Sshkey", () => {
KeyFingerprint: "keyFingerprint", KeyFingerprint: "keyFingerprint",
}), }),
); );
mockContainerService();
}); });
it("Convert", () => { it("Convert", () => {

View File

@@ -24,16 +24,11 @@ export class SshKey extends Domain {
this.keyFingerprint = new EncString(obj.keyFingerprint); this.keyFingerprint = new EncString(obj.keyFingerprint);
} }
decrypt( decrypt(encKey: SymmetricCryptoKey, context = "No Cipher Context"): Promise<SshKeyView> {
orgId: string | undefined,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
): Promise<SshKeyView> {
return this.decryptObj<SshKey, SshKeyView>( return this.decryptObj<SshKey, SshKeyView>(
this, this,
new SshKeyView(), new SshKeyView(),
["privateKey", "publicKey", "keyFingerprint"], ["privateKey", "publicKey", "keyFingerprint"],
orgId ?? null,
encKey, encKey,
"DomainType: SshKey; " + context, "DomainType: SshKey; " + context,
); );

View File

@@ -55,7 +55,7 @@ const ENCRYPTED_BYTES = mock<EncArrayBuffer>();
const cipherData: CipherData = { const cipherData: CipherData = {
id: "id", id: "id",
organizationId: "orgId", organizationId: "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b2" as OrganizationId,
folderId: "folderId", folderId: "folderId",
edit: true, edit: true,
viewPassword: true, viewPassword: true,
@@ -119,6 +119,8 @@ describe("Cipher Service", () => {
beforeEach(() => { beforeEach(() => {
encryptService.encryptFileData.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES)); encryptService.encryptFileData.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES));
encryptService.encryptString.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT))); encryptService.encryptString.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT)));
keyService.orgKeys$.mockReturnValue(of({ [orgId]: makeSymmetricCryptoKey(32) as OrgKey }));
keyService.userKey$.mockReturnValue(of(makeSymmetricCryptoKey(64) as UserKey));
// Mock i18nService collator // Mock i18nService collator
i18nService.collator = { i18nService.collator = {
@@ -181,9 +183,6 @@ describe("Cipher Service", () => {
const testCipher = new Cipher(cipherData); const testCipher = new Cipher(cipherData);
const expectedRevisionDate = "2022-01-31T12:00:00.000Z"; const expectedRevisionDate = "2022-01-31T12:00:00.000Z";
keyService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey),
);
keyService.makeDataEncKey.mockReturnValue( keyService.makeDataEncKey.mockReturnValue(
Promise.resolve([ Promise.resolve([
new SymmetricCryptoKey(new Uint8Array(32)), new SymmetricCryptoKey(new Uint8Array(32)),

View File

@@ -1564,10 +1564,15 @@ export class CipherService implements CipherServiceAbstraction {
} }
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> { async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
return ( if (cipher.organizationId == null) {
(await this.keyService.getOrgKey(cipher.organizationId)) || return await firstValueFrom(this.keyService.userKey$(userId));
((await this.keyService.getUserKey(userId)) as UserKey) } else {
); return await firstValueFrom(
this.keyService
.orgKeys$(userId)
.pipe(map((orgKeys) => orgKeys[cipher.organizationId as OrganizationId] as OrgKey)),
);
}
} }
async setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId) { async setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId) {

View File

@@ -1,6 +1,6 @@
<span class="tw-relative tw-inline-block tw-leading-[0px]"> <span class="tw-relative tw-inline-block tw-leading-[0px]">
<span class="tw-inline-block tw-leading-[0px]" [ngClass]="{ 'tw-invisible': showLoadingStyle() }"> <span class="tw-inline-block tw-leading-[0px]" [ngClass]="{ 'tw-invisible': showLoadingStyle() }">
<i class="bwi" [ngClass]="iconClass" aria-hidden="true"></i> <i class="bwi" [ngClass]="iconClass()" aria-hidden="true"></i>
</span> </span>
<span <span
class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center" class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center"

View File

@@ -20,7 +20,16 @@ import { SpinnerComponent } from "../spinner";
import { TooltipDirective } from "../tooltip"; import { TooltipDirective } from "../tooltip";
import { ariaDisableElement } from "../utils"; import { ariaDisableElement } from "../utils";
export type IconButtonType = "primary" | "danger" | "contrast" | "main" | "muted" | "nav-contrast"; export const IconButtonTypes = [
"primary",
"danger",
"contrast",
"main",
"muted",
"nav-contrast",
] as const;
export type IconButtonType = (typeof IconButtonTypes)[number];
const focusRing = [ const focusRing = [
// Workaround for box-shadow with transparent offset issue: // Workaround for box-shadow with transparent offset issue:
@@ -148,9 +157,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
); );
} }
get iconClass() { readonly iconClass = computed(() => [this.icon(), "!tw-m-0"]);
return [this.icon(), "!tw-m-0"];
}
protected readonly disabledAttr = computed(() => { protected readonly disabledAttr = computed(() => {
const disabled = this.disabled() != null && this.disabled() !== false; const disabled = this.disabled() != null && this.disabled() !== false;

View File

@@ -5,7 +5,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet"; import { formatArgsForCodeSnippet } from "../../../../.storybook/format-args-for-code-snippet";
import { I18nMockService } from "../utils"; import { I18nMockService } from "../utils";
import { BitIconButtonComponent } from "./icon-button.component"; import { BitIconButtonComponent, IconButtonTypes } from "./icon-button.component";
export default { export default {
title: "Component Library/Icon Button", title: "Component Library/Icon Button",
@@ -30,7 +30,7 @@ export default {
}, },
argTypes: { argTypes: {
buttonType: { buttonType: {
options: ["primary", "secondary", "danger", "unstyled", "contrast", "main", "muted", "light"], options: IconButtonTypes,
}, },
}, },
parameters: { parameters: {

View File

@@ -6,7 +6,6 @@ import { filter, firstValueFrom } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { Collection, CollectionView } from "@bitwarden/admin-console/common"; import { Collection, CollectionView } from "@bitwarden/admin-console/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { import {
@@ -46,6 +45,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
} }
async parse(data: string): Promise<ImportResult> { async parse(data: string): Promise<ImportResult> {
const account = await firstValueFrom(this.accountService.activeAccount$);
this.result = new ImportResult(); this.result = new ImportResult();
const results: BitwardenJsonExport = JSON.parse(data); const results: BitwardenJsonExport = JSON.parse(data);
if (results == null || results.items == null) { if (results == null || results.items == null) {
@@ -54,9 +54,9 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
} }
if (results.encrypted) { if (results.encrypted) {
await this.parseEncrypted(results as any); await this.parseEncrypted(results as any, account.id);
} else { } else {
await this.parseDecrypted(results as any); await this.parseDecrypted(results as any, account.id);
} }
return this.result; return this.result;
@@ -64,9 +64,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
private async parseEncrypted( private async parseEncrypted(
results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport, results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport,
userId: UserId,
) { ) {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (results.encKeyValidation_DO_NOT_EDIT != null) { if (results.encKeyValidation_DO_NOT_EDIT != null) {
const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId)); const orgKeys = await firstValueFrom(this.keyService.orgKeys$(userId));
let keyForDecryption: SymmetricCryptoKey = orgKeys?.[this.organizationId]; let keyForDecryption: SymmetricCryptoKey = orgKeys?.[this.organizationId];
@@ -84,8 +83,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
} }
const groupingsMap = this.organization const groupingsMap = this.organization
? await this.parseCollections(userId, results as BitwardenEncryptedOrgJsonExport) ? await this.parseCollections(results as BitwardenEncryptedOrgJsonExport, userId)
: await this.parseFolders(results as BitwardenEncryptedIndividualJsonExport); : await this.parseFolders(results as BitwardenEncryptedIndividualJsonExport, userId);
for (const c of results.items) { for (const c of results.items) {
const cipher = CipherWithIdExport.toDomain(c); const cipher = CipherWithIdExport.toDomain(c);
@@ -125,12 +124,11 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
private async parseDecrypted( private async parseDecrypted(
results: BitwardenUnEncryptedIndividualJsonExport | BitwardenUnEncryptedOrgJsonExport, results: BitwardenUnEncryptedIndividualJsonExport | BitwardenUnEncryptedOrgJsonExport,
userId: UserId,
) { ) {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const groupingsMap = this.organization const groupingsMap = this.organization
? await this.parseCollections(userId, results as BitwardenUnEncryptedOrgJsonExport) ? await this.parseCollections(results as BitwardenUnEncryptedOrgJsonExport, userId)
: await this.parseFolders(results as BitwardenUnEncryptedIndividualJsonExport); : await this.parseFolders(results as BitwardenUnEncryptedIndividualJsonExport, userId);
results.items.forEach((c) => { results.items.forEach((c) => {
const cipher = CipherWithIdExport.toView(c); const cipher = CipherWithIdExport.toView(c);
@@ -169,11 +167,14 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
private async parseFolders( private async parseFolders(
data: BitwardenUnEncryptedIndividualJsonExport | BitwardenEncryptedIndividualJsonExport, data: BitwardenUnEncryptedIndividualJsonExport | BitwardenEncryptedIndividualJsonExport,
userId: UserId,
): Promise<Map<string, number>> | null { ): Promise<Map<string, number>> | null {
if (data.folders == null) { if (data.folders == null) {
return null; return null;
} }
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
const groupingsMap = new Map<string, number>(); const groupingsMap = new Map<string, number>();
for (const f of data.folders) { for (const f of data.folders) {
@@ -181,7 +182,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
if (data.encrypted) { if (data.encrypted) {
const folder = FolderWithIdExport.toDomain(f); const folder = FolderWithIdExport.toDomain(f);
if (folder != null) { if (folder != null) {
folderView = await folder.decrypt(); folderView = await folder.decrypt(userKey);
} }
} else { } else {
folderView = FolderWithIdExport.toView(f); folderView = FolderWithIdExport.toView(f);
@@ -196,8 +197,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
} }
private async parseCollections( private async parseCollections(
userId: UserId,
data: BitwardenUnEncryptedOrgJsonExport | BitwardenEncryptedOrgJsonExport, data: BitwardenUnEncryptedOrgJsonExport | BitwardenEncryptedOrgJsonExport,
userId: UserId,
): Promise<Map<string, number>> | null { ): Promise<Map<string, number>> | null {
if (data.collections == null) { if (data.collections == null) {
return null; return null;

1004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@
"@angular/compiler-cli": "20.3.15", "@angular/compiler-cli": "20.3.15",
"@babel/core": "7.28.5", "@babel/core": "7.28.5",
"@babel/preset-env": "7.28.5", "@babel/preset-env": "7.28.5",
"@compodoc/compodoc": "1.1.26", "@compodoc/compodoc": "1.1.32",
"@electron/notarize": "3.0.1", "@electron/notarize": "3.0.1",
"@electron/rebuild": "4.0.1", "@electron/rebuild": "4.0.1",
"@eslint/compat": "2.0.0", "@eslint/compat": "2.0.0",
@@ -87,7 +87,7 @@
"axe-playwright": "2.2.2", "axe-playwright": "2.2.2",
"babel-loader": "9.2.1", "babel-loader": "9.2.1",
"base64-loader": "1.0.0", "base64-loader": "1.0.0",
"browserslist": "4.28.0", "browserslist": "4.28.1",
"chromatic": "13.3.1", "chromatic": "13.3.1",
"concurrently": "9.2.0", "concurrently": "9.2.0",
"copy-webpack-plugin": "13.0.1", "copy-webpack-plugin": "13.0.1",