1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 03:03:26 +00:00

Remove decrypt from encstring

This commit is contained in:
Bernd Schoolmann
2025-10-27 13:02:55 +01:00
parent e8db35907d
commit 5aee44b5c2
47 changed files with 195 additions and 220 deletions

View File

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

View File

@@ -420,7 +420,7 @@ export class GetCommand extends DownloadCommand {
if (Utils.isGuid(id)) {
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder != null) {
decFolder = await folder.decrypt();
decFolder = await folder.decrypt(activeUserId);
}
} else if (id.trim() !== "") {
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);

View File

@@ -5,6 +5,8 @@ import * as inquirer from "inquirer";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -37,6 +39,7 @@ export class SendReceiveCommand extends DownloadCommand {
private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService,
private sendApiService: SendApiService,
private accountService: AccountService,
apiService: ApiService,
) {
super(encryptService, apiService);
@@ -152,6 +155,8 @@ export class SendReceiveCommand extends DownloadCommand {
key: Uint8Array,
): Promise<Response | SendAccessView> {
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const sendResponse = await this.sendApiService.postSendAccess(
id,
this.sendAccessRequest,
@@ -160,7 +165,7 @@ export class SendReceiveCommand extends DownloadCommand {
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.keyService.makeSendKey(key);
return await sendAccess.decrypt(this.decKey);
return await sendAccess.decrypt(activeUserId, this.decKey);
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {

View File

@@ -123,6 +123,7 @@ export class SendProgram extends BaseProgram {
this.serviceContainer.platformUtilsService,
this.serviceContainer.environmentService,
this.serviceContainer.sendApiService,
this.serviceContainer.accountService,
this.serviceContainer.apiService,
);
const response = await cmd.run(url, options);

View File

@@ -181,12 +181,12 @@ export class CreateCommand {
private async createFolder(req: FolderExport) {
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);
try {
const folderData = await this.folderApiService.save(folder, activeUserId);
const newFolder = new Folder(folderData);
const decFolder = await newFolder.decrypt();
const decFolder = await newFolder.decrypt(activeUserId);
const res = new FolderResponse(decFolder);
return Response.success(res);
} catch (e) {

View File

@@ -264,7 +264,9 @@ export class EmergencyAccessService
let ciphers: CipherView[] = [];
const ciphersEncrypted = response.ciphers.map((c) => new Cipher(c));
ciphers = await Promise.all(ciphersEncrypted.map(async (c) => c.decrypt(grantorUserKey)));
ciphers = await Promise.all(
ciphersEncrypted.map(async (c) => c.decrypt(grantorUserKey, activeUserId)),
);
return ciphers.sort(this.cipherService.getLocaleSortingFunction());
}

View File

@@ -239,6 +239,7 @@ export class CipherReportComponent implements OnDestroy {
// convert cipher to cipher view model
const updatedCipherView = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
activeUserId,
);
// request downstream report status if the cipher was updated

View File

@@ -3,7 +3,10 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -64,6 +67,7 @@ export class AccessComponent implements OnInit {
private i18nService: I18nService,
private layoutWrapperDataService: AnonLayoutWrapperDataService,
protected formBuilder: FormBuilder,
private accountService: AccountService,
) {}
protected get expirationDate() {
@@ -118,7 +122,8 @@ export class AccessComponent implements OnInit {
this.passwordRequired = false;
const sendAccess = new SendAccess(sendResponse);
this.decKey = await this.keyService.makeSendKey(keyArray);
this.send = await sendAccess.decrypt(this.decKey);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.send = await sendAccess.decrypt(activeUserId, this.decKey);
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {

View File

@@ -498,6 +498,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
updatedCipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
activeUserId,
);
} else {
const updatedCipher = await this.cipherService.get(

View File

@@ -27,10 +27,9 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
}
/** @deprecated */
export function mockEnc(s: string): MockProxy<EncString> {
const mocked = mock<EncString>();
mocked.decrypt.mockResolvedValue(s);
return mocked;
}

View File

@@ -1,15 +1,4 @@
import { mock, MockProxy } from "jest-mock-extended";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "@bitwarden/key-management";
import { makeStaticByteArray } from "../../../../spec";
import { EncryptionType } from "../../../platform/enums";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { ContainerService } from "../../../platform/services/container.service";
import { UserKey, OrgKey } from "../../../types/key";
import { EncryptService } from "../abstractions/encrypt.service";
import { EncString } from "./enc-string";
@@ -77,41 +66,6 @@ describe("EncString", () => {
expect(dataBytes.length).toBeGreaterThan(0);
});
});
describe("decrypt", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const keyService = mock<KeyService>();
keyService.hasUserKey.mockResolvedValue(true);
keyService.getUserKey.mockResolvedValue(
new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey,
);
const encryptService = mock<EncryptService>();
encryptService.decryptString
.calledWith(encString, expect.anything())
.mockResolvedValue("decrypted");
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(
keyService,
encryptService,
);
});
it("decrypts correctly", async () => {
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("decrypted");
});
it("result should be cached", async () => {
const decrypted = await encString.decrypt(null);
expect(encryptService.decryptString).toBeCalledTimes(1);
expect(decrypted).toBe("decrypted");
});
});
});
describe("AesCbc256_B64", () => {
@@ -249,66 +203,6 @@ describe("EncString", () => {
});
});
describe("decrypt", () => {
let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>;
let encString: EncString;
beforeEach(() => {
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
encString = new EncString(null);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
});
it("handles value it can't decrypt", async () => {
encryptService.decryptString.mockRejectedValue("error");
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("[error: cannot decrypt]");
expect(encString).toEqual({
decryptedValue: "[error: cannot decrypt]",
encryptedString: null,
});
});
it("uses provided key without depending on KeyService", async () => {
const key = mock<SymmetricCryptoKey>();
await encString.decrypt(null, key);
expect(keyService.getUserKey).not.toHaveBeenCalled();
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, key);
});
it("gets an organization key if required", async () => {
const orgKey = mock<OrgKey>();
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await encString.decrypt("orgId", null);
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, orgKey);
});
it("gets the user's decryption key if required", async () => {
const userKey = mock<UserKey>();
keyService.getUserKey.mockResolvedValue(userKey);
await encString.decrypt(null, null);
expect(keyService.getUserKey).toHaveBeenCalledWith();
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, userKey);
});
});
describe("toJSON", () => {
it("Should be represented by the encrypted string", () => {
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");

View File

@@ -2,11 +2,10 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString as SdkEncString } from "@bitwarden/sdk-internal";
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export const DECRYPT_ERROR = "[error: cannot decrypt]";
@@ -156,46 +155,6 @@ export class EncString {
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
}
/**
* @deprecated - This function is deprecated. Use EncryptService.decryptString instead.
* @returns - The decrypted string, or `[error: cannot decrypt]` if decryption fails.
*/
async decrypt(
orgId: string | null,
key: SymmetricCryptoKey | null = null,
context?: string,
): Promise<string> {
if (this.decryptedValue != null) {
return this.decryptedValue;
}
try {
if (key == null) {
key = await this.getKeyForDecryption(orgId);
}
if (key == null) {
throw new Error("No key to decrypt EncString with orgId " + orgId);
}
const encryptService = Utils.getContainerService().getEncryptService();
this.decryptedValue = await encryptService.decryptString(this, key);
} catch (e) {
// eslint-disable-next-line no-console
console.error(
"[EncString Generic Decrypt] failed to decrypt encstring. Context: " +
(context ?? "No context"),
e,
);
this.decryptedValue = DECRYPT_ERROR;
}
return this.decryptedValue;
}
private async getKeyForDecryption(orgId: string) {
const keyService = Utils.getContainerService().getKeyService();
return orgId != null ? await keyService.getOrgKey(orgId) : await keyService.getUserKey();
}
}
/**

View File

@@ -1,3 +1,5 @@
import { UserId } from "@bitwarden/user-core";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { InitializerMetadata } from "./initializer-metadata.interface";
@@ -8,5 +10,5 @@ import { InitializerMetadata } from "./initializer-metadata.interface";
* @example Cipher implements Decryptable<CipherView>
*/
export interface Decryptable<TDecrypted extends InitializerMetadata> extends InitializerMetadata {
decrypt: (key: SymmetricCryptoKey) => Promise<TDecrypted>;
decrypt: (key: SymmetricCryptoKey, userId: UserId) => Promise<TDecrypted>;
}

View File

@@ -1,7 +1,12 @@
import { firstValueFrom, map } from "rxjs";
import { ConditionalExcept, ConditionalKeys } from "type-fest";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { View } from "../../../models/view/view";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
@@ -69,21 +74,34 @@ export default class Domain {
}
}
/** @deprecated */
protected async decryptObj<D extends Domain, V extends View>(
domain: DomainEncryptableKeys<D>,
viewModel: ViewEncryptableKeys<V>,
props: EncryptableKeys<D, V>[],
userId: UserId,
orgId: string | null,
key: SymmetricCryptoKey | null = null,
objectContext: string = "No Domain Context",
_objectContext: string = "No Domain Context",
): Promise<V> {
const keyService = Utils.getContainerService().getKeyService();
if (key == null) {
if (orgId != null) {
key = await firstValueFrom(
keyService
.orgKeys$(userId)
.pipe(map((orgKeys) => orgKeys![orgId as OrganizationId] ?? null)),
);
} else {
key = await firstValueFrom(keyService.userKey$(userId));
}
}
const encService = Utils.getContainerService().getEncryptService();
for (const prop of props) {
viewModel[prop] =
(await domain[prop]?.decrypt(
orgId,
key,
`Property: ${prop as string}; ObjectContext: ${objectContext}`,
)) ?? null;
if (domain[prop] != null) {
viewModel[prop] = await encService.decryptString(domain[prop], key!);
}
}
return viewModel as V;

View File

@@ -67,7 +67,7 @@ describe("SendAccess", () => {
sendAccess.expirationDate = new Date("2022-01-31T12:00:00.000Z");
sendAccess.creatorIdentifier = "creatorIdentifier";
const view = await sendAccess.decrypt(null);
const view = await sendAccess.decrypt(null, null);
expect(text.decrypt).toHaveBeenCalledTimes(1);

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../../key-management/crypto/models/enc-string";
import Domain from "../../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
@@ -51,17 +53,17 @@ export class SendAccess extends Domain {
}
}
async decrypt(key: SymmetricCryptoKey): Promise<SendAccessView> {
async decrypt(userId: UserId, key: SymmetricCryptoKey): Promise<SendAccessView> {
const model = new SendAccessView(this);
await this.decryptObj<SendAccess, SendAccessView>(this, model, ["name"], null, key);
await this.decryptObj<SendAccess, SendAccessView>(this, model, ["name"], userId, null, key);
switch (this.type) {
case SendType.File:
model.file = await this.file.decrypt(key);
model.file = await this.file.decrypt(userId, key);
break;
case SendType.Text:
model.text = await this.text.decrypt(key);
model.text = await this.text.decrypt(userId, key);
break;
default:
break;

View File

@@ -45,7 +45,7 @@ describe("SendFile", () => {
sendFile.sizeName = "1.1 KB";
sendFile.fileName = mockEnc("fileName");
const view = await sendFile.decrypt(null);
const view = await sendFile.decrypt(null, null);
expect(view).toEqual({
fileName: "fileName",

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../../key-management/crypto/models/enc-string";
import Domain from "../../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
@@ -33,11 +35,12 @@ export class SendFile extends Domain {
);
}
async decrypt(key: SymmetricCryptoKey): Promise<SendFileView> {
async decrypt(userId: UserId, key: SymmetricCryptoKey): Promise<SendFileView> {
return await this.decryptObj<SendFile, SendFileView>(
this,
new SendFileView(this),
["fileName"],
userId,
null,
key,
);

View File

@@ -37,7 +37,7 @@ describe("SendText", () => {
secureNote.text = mockEnc("text");
secureNote.hidden = true;
const view = await secureNote.decrypt(null);
const view = await secureNote.decrypt(null, null);
expect(view).toEqual({
text: "text",

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../../key-management/crypto/models/enc-string";
import Domain from "../../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
@@ -29,11 +31,12 @@ export class SendText extends Domain {
);
}
decrypt(key: SymmetricCryptoKey): Promise<SendTextView> {
decrypt(userId: UserId, key: SymmetricCryptoKey): Promise<SendTextView> {
return this.decryptObj<SendText, SendTextView>(
this,
new SendTextView(this),
["text"],
userId,
null,
key,
);

View File

@@ -130,12 +130,6 @@ describe("Send", () => {
const view = await send.decrypt(userId);
expect(text.decrypt).toHaveBeenNthCalledWith(1, "cryptoKey");
expect(send.name.decrypt).toHaveBeenNthCalledWith(
1,
null,
"cryptoKey",
"Property: name; ObjectContext: No Domain Context",
);
expect(view).toMatchObject({
id: "id",

View File

@@ -89,14 +89,21 @@ export class Send extends Domain {
model.key = await encryptService.decryptBytes(this.key, sendKeyEncryptionKey);
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"],
userId,
null,
model.cryptoKey,
);
switch (this.type) {
case SendType.File:
model.file = await this.file.decrypt(model.cryptoKey);
model.file = await this.file.decrypt(userId, model.cryptoKey);
break;
case SendType.Text:
model.text = await this.text.decrypt(model.cryptoKey);
model.text = await this.text.decrypt(userId, model.cryptoKey);
break;
default:
break;

View File

@@ -86,7 +86,7 @@ describe("Attachment", () => {
new SymmetricCryptoKey(makeStaticByteArray(64)),
);
const view = await attachment.decrypt(null);
const view = await attachment.decrypt(null, null);
expect(view).toEqual({
id: "id",
@@ -110,7 +110,7 @@ describe("Attachment", () => {
it("uses the provided key without depending on KeyService", async () => {
const providedKey = mock<SymmetricCryptoKey>();
await attachment.decrypt(null, "", providedKey);
await attachment.decrypt(null, null, "", providedKey);
expect(keyService.getUserKey).not.toHaveBeenCalled();
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, providedKey);
@@ -120,7 +120,7 @@ describe("Attachment", () => {
const orgKey = mock<OrgKey>();
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
await attachment.decrypt("orgId", "", null);
await attachment.decrypt(null, "orgId", "", null);
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.unwrapSymmetricKey).toHaveBeenCalledWith(attachment.key, orgKey);

View File

@@ -2,6 +2,7 @@ import { Jsonify } from "type-fest";
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { Utils } from "../../../platform/misc/utils";
@@ -34,6 +35,7 @@ export class Attachment extends Domain {
}
async decrypt(
userId: UserId,
orgId: string | undefined,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
@@ -42,6 +44,7 @@ export class Attachment extends Domain {
this,
new AttachmentView(this),
["fileName"],
userId,
orgId ?? null,
encKey,
"DomainType: Attachment; " + context,

View File

@@ -58,7 +58,7 @@ describe("Card", () => {
card.expYear = mockEnc("expYear");
card.code = mockEnc("code");
const view = await card.decrypt(null);
const view = await card.decrypt(null, null);
expect(view).toEqual({
_brand: "brand",

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { Card as SdkCard } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -32,6 +33,7 @@ export class Card extends Domain {
}
async decrypt(
userId: UserId,
orgId: string | undefined,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
@@ -40,6 +42,7 @@ export class Card extends Domain {
this,
new CardView(),
["cardholderName", "brand", "number", "expMonth", "expYear", "code"],
userId,
orgId ?? null,
encKey,
"DomainType: Card; " + context,

View File

@@ -105,6 +105,7 @@ describe("Cipher DTO", () => {
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
null,
);
expect(cipherView).toMatchObject({
@@ -329,6 +330,7 @@ describe("Cipher DTO", () => {
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
null,
);
expect(cipherView).toMatchObject({
@@ -457,6 +459,7 @@ describe("Cipher DTO", () => {
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
null,
);
expect(cipherView).toMatchObject({
@@ -603,6 +606,7 @@ describe("Cipher DTO", () => {
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
null,
);
expect(cipherView).toMatchObject({
@@ -773,6 +777,7 @@ describe("Cipher DTO", () => {
const cipherView = await cipher.decrypt(
await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId),
null,
);
expect(cipherView).toMatchObject({

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { asUuid, uuidAsString } from "../../../platform/abstractions/sdk/sdk.service";
@@ -123,7 +124,7 @@ 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
// present and so the organizationId will not be used.
// 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> {
async decrypt(encKey: SymmetricCryptoKey, userId: UserId): Promise<CipherView> {
const model = new CipherView(this);
let bypassValidation = true;
@@ -145,14 +146,17 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
this,
model,
["name", "notes"],
userId,
this.organizationId ?? null,
encKey,
"cipher",
);
switch (this.type) {
case CipherType.Login:
if (this.login != null) {
model.login = await this.login.decrypt(
userId,
this.organizationId,
bypassValidation,
`Cipher Id: ${this.id}`,
@@ -168,6 +172,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
case CipherType.Card:
if (this.card != null) {
model.card = await this.card.decrypt(
userId,
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
@@ -177,6 +182,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
case CipherType.Identity:
if (this.identity != null) {
model.identity = await this.identity.decrypt(
userId,
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
@@ -186,6 +192,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
case CipherType.SshKey:
if (this.sshKey != null) {
model.sshKey = await this.sshKey.decrypt(
userId,
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
@@ -200,6 +207,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
const attachments: AttachmentView[] = [];
for (const attachment of this.attachments) {
const decryptedAttachment = await attachment.decrypt(
userId,
this.organizationId,
`Cipher Id: ${this.id}`,
encKey,
@@ -212,7 +220,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.fields != null && this.fields.length > 0) {
const fields: FieldView[] = [];
for (const field of this.fields) {
const decryptedField = await field.decrypt(this.organizationId, encKey);
const decryptedField = await field.decrypt(userId, this.organizationId, encKey);
fields.push(decryptedField);
}
model.fields = fields;
@@ -221,7 +229,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
if (this.passwordHistory != null && this.passwordHistory.length > 0) {
const passwordHistory: PasswordHistoryView[] = [];
for (const ph of this.passwordHistory) {
const decryptedPh = await ph.decrypt(this.organizationId, encKey);
const decryptedPh = await ph.decrypt(userId, this.organizationId, encKey);
passwordHistory.push(decryptedPh);
}
model.passwordHistory = passwordHistory;

View File

@@ -103,7 +103,7 @@ describe("Fido2Credential", () => {
credential.discoverable = mockEnc("true");
credential.creationDate = mockDate;
const credentialView = await credential.decrypt(null);
const credentialView = await credential.decrypt(null, null);
expect(credentialView).toEqual({
credentialId: "credentialId",

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -47,6 +48,7 @@ export class Fido2Credential extends Domain {
}
async decrypt(
userId: UserId,
orgId: string | undefined,
encKey?: SymmetricCryptoKey,
): Promise<Fido2CredentialView> {
@@ -65,6 +67,7 @@ export class Fido2Credential extends Domain {
"rpName",
"userDisplayName",
],
userId,
orgId ?? null,
encKey,
);
@@ -74,7 +77,7 @@ export class Fido2Credential extends Domain {
{
counter: string;
}
>(this, { counter: "" }, ["counter"], orgId ?? null, encKey);
>(this, { counter: "" }, ["counter"], userId, orgId ?? null, encKey);
// Counter will end up as NaN if this fails
view.counter = parseInt(counter);
@@ -82,6 +85,7 @@ export class Fido2Credential extends Domain {
this,
{ discoverable: "" },
["discoverable"],
userId,
orgId ?? null,
encKey,
);

View File

@@ -58,7 +58,7 @@ describe("Field", () => {
field.name = mockEnc("encName");
field.value = mockEnc("encValue");
const view = await field.decrypt(null);
const view = await field.decrypt(null, null);
expect(view).toEqual({
type: 0,

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -28,11 +29,16 @@ export class Field extends Domain {
this.value = conditionalEncString(obj.value);
}
decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise<FieldView> {
decrypt(
userId: UserId,
orgId: string | undefined,
encKey?: SymmetricCryptoKey,
): Promise<FieldView> {
return this.decryptObj<Field, FieldView>(
this,
new FieldView(this),
["name", "value"],
userId,
orgId ?? null,
encKey,
);

View File

@@ -33,7 +33,7 @@ describe("Folder", () => {
folder.name = mockEnc("encName");
folder.revisionDate = new Date("2022-01-31T12:00:00.000Z");
const view = await folder.decrypt();
const view = await folder.decrypt(null);
expect(view).toEqual({
id: "id",

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { UserId } from "@bitwarden/user-core";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -39,8 +41,8 @@ export class Folder extends Domain {
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
}
decrypt(): Promise<FolderView> {
return this.decryptObj<Folder, FolderView>(this, new FolderView(this), ["name"], null);
decrypt(userId: UserId): Promise<FolderView> {
return this.decryptObj<Folder, FolderView>(this, new FolderView(this), ["name"], userId, null);
}
async decryptWithKey(

View File

@@ -107,7 +107,7 @@ describe("Identity", () => {
identity.passportNumber = mockEnc("mockPassportNumber");
identity.licenseNumber = mockEnc("mockLicenseNumber");
const view = await identity.decrypt(null);
const view = await identity.decrypt(null, null);
expect(view).toEqual({
_firstName: "mockFirstName",

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { Identity as SdkIdentity } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -56,6 +57,7 @@ export class Identity extends Domain {
}
decrypt(
userId: UserId,
orgId: string | undefined,
context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
@@ -83,6 +85,7 @@ export class Identity extends Domain {
"passportNumber",
"licenseNumber",
],
userId,
orgId ?? null,
encKey,
"DomainType: Identity; " + context,

View File

@@ -53,7 +53,7 @@ describe("LoginUri", () => {
loginUri.match = UriMatchStrategy.Exact;
loginUri.uri = mockEnc("uri");
const view = await loginUri.decrypt(null);
const view = await loginUri.decrypt(null, null);
expect(view).toEqual({
_uri: "uri",
@@ -77,7 +77,7 @@ describe("LoginUri", () => {
loginUri.uriChecksum = mockEnc("checksum");
encryptService.hash.mockResolvedValue("checksum");
const actual = await loginUri.validateChecksum("uri", undefined, undefined);
const actual = await loginUri.validateChecksum("uri", undefined, undefined, undefined);
expect(actual).toBe(true);
expect(encryptService.hash).toHaveBeenCalledWith("uri", "sha256");
@@ -88,7 +88,7 @@ describe("LoginUri", () => {
loginUri.uriChecksum = mockEnc("checksum");
encryptService.hash.mockResolvedValue("incorrect checksum");
const actual = await loginUri.validateChecksum("uri", undefined, undefined);
const actual = await loginUri.validateChecksum("uri", undefined, undefined, undefined);
expect(actual).toBe(false);
});

View File

@@ -1,6 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs";
import { Jsonify } from "type-fest";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
@@ -28,6 +33,7 @@ export class LoginUri extends Domain {
}
decrypt(
userId: UserId,
orgId: string | undefined,
context: string = "No Cipher Context",
encKey?: SymmetricCryptoKey,
@@ -36,21 +42,41 @@ export class LoginUri extends Domain {
this,
new LoginUriView(this),
["uri"],
userId,
orgId ?? null,
encKey,
context,
);
}
async validateChecksum(clearTextUri: string, orgId?: string, encKey?: SymmetricCryptoKey) {
async validateChecksum(
clearTextUri: string,
userId: UserId,
orgId?: string,
key?: SymmetricCryptoKey,
) {
if (this.uriChecksum == null) {
return false;
}
const keyService = Utils.getContainerService().getEncryptService();
const localChecksum = await keyService.hash(clearTextUri, "sha256");
const keyService = Utils.getContainerService().getKeyService();
const encService = Utils.getContainerService().getEncryptService();
const localChecksum = await encService.hash(clearTextUri, "sha256");
const remoteChecksum = await this.uriChecksum.decrypt(orgId ?? null, encKey);
if (key == null) {
if (orgId != null) {
key = await firstValueFrom(
keyService
.orgKeys$(userId)
.pipe(map((orgKeys) => orgKeys[orgId as OrganizationId] ?? null)),
);
} else {
key = await firstValueFrom(keyService.userKey$(userId));
}
}
const remoteChecksum = await encService.decryptString(this.uriChecksum, key);
/// WARNING: This is not constant time. This should be moved to the SDK
return remoteChecksum === localChecksum;
}

View File

@@ -99,7 +99,7 @@ describe("Login DTO", () => {
loginUri.validateChecksum.mockResolvedValue(true);
login.uris = [loginUri];
const loginView = await login.decrypt(null, true);
const loginView = await login.decrypt(null, null, true);
expect(loginView).toEqual(expectedView);
});
@@ -111,7 +111,7 @@ describe("Login DTO", () => {
.mockResolvedValueOnce(true);
login.uris = [loginUri, loginUri, loginUri];
const loginView = await login.decrypt(null, false);
const loginView = await login.decrypt(null, null, false);
expect(loginView).toEqual(expectedView);
});
});

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { Login as SdkLogin } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -44,6 +45,7 @@ export class Login extends Domain {
}
async decrypt(
userId: UserId,
orgId: string | undefined,
bypassValidation: boolean,
context: string = "No Cipher Context",
@@ -53,6 +55,7 @@ export class Login extends Domain {
this,
new LoginView(this),
["username", "password", "totp"],
userId,
orgId ?? null,
encKey,
`DomainType: Login; ${context}`,
@@ -66,7 +69,7 @@ export class Login extends Domain {
continue;
}
const uri = await this.uris[i].decrypt(orgId, context, encKey);
const uri = await this.uris[i].decrypt(userId, orgId, context, encKey);
const uriString = uri.uri;
if (uriString == null) {
@@ -79,7 +82,8 @@ export class Login extends Domain {
// 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.
const isValidUri =
bypassValidation || (await this.uris[i].validateChecksum(uriString, orgId, encKey));
bypassValidation ||
(await this.uris[i].validateChecksum(uriString, userId, orgId, encKey));
if (isValidUri) {
view.uris.push(uri);
@@ -89,7 +93,7 @@ export class Login extends Domain {
if (this.fido2Credentials != null) {
view.fido2Credentials = await Promise.all(
this.fido2Credentials.map((key) => key.decrypt(orgId, encKey)),
this.fido2Credentials.map((key) => key.decrypt(userId, orgId, encKey)),
);
}

View File

@@ -41,7 +41,7 @@ describe("Password", () => {
password.password = mockEnc("password");
password.lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
const view = await password.decrypt(null);
const view = await password.decrypt(null, null);
expect(view).toEqual({
password: "password",

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { PasswordHistory } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -22,11 +23,16 @@ export class Password extends Domain {
this.lastUsedDate = new Date(obj.lastUsedDate);
}
decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise<PasswordHistoryView> {
decrypt(
userId: UserId,
orgId: string | undefined,
encKey?: SymmetricCryptoKey,
): Promise<PasswordHistoryView> {
return this.decryptObj<Password, PasswordHistoryView>(
this,
new PasswordHistoryView(this),
["password"],
userId,
orgId ?? null,
encKey,
"DomainType: PasswordHistory",

View File

@@ -56,7 +56,7 @@ describe("Sshkey", () => {
keyFingerprint: "keyFingerprint",
};
const loginView = await sshKey.decrypt(null);
const loginView = await sshKey.decrypt(null, null);
expect(loginView).toEqual(expectedView);
});

View File

@@ -1,6 +1,7 @@
import { Jsonify } from "type-fest";
import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal";
import { UserId } from "@bitwarden/user-core";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import Domain from "../../../platform/models/domain/domain-base";
@@ -25,6 +26,7 @@ export class SshKey extends Domain {
}
decrypt(
userId: UserId,
orgId: string | undefined,
context = "No Cipher Context",
encKey?: SymmetricCryptoKey,
@@ -33,6 +35,7 @@ export class SshKey extends Domain {
this,
new SshKeyView(),
["privateKey", "publicKey", "keyFingerprint"],
userId,
orgId ?? null,
encKey,
"DomainType: SshKey; " + context,

View File

@@ -12,6 +12,7 @@ import {
} from "rxjs";
import { SemVer } from "semver";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessageSender } from "@bitwarden/common/platform/messaging";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
@@ -520,7 +521,7 @@ export class CipherService implements CipherServiceAbstraction {
const key = keys.orgKeys[orgId as OrganizationId] ?? keys.userKey;
return await Promise.all(
groupedCiphers.map(async (cipher) => {
return await cipher.decrypt(key);
return await cipher.decrypt(key, userId);
}),
);
}),
@@ -558,7 +559,7 @@ export class CipherService implements CipherServiceAbstraction {
return await this.cipherEncryptionService.decrypt(cipher, userId);
} else {
const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId);
return await cipher.decrypt(encKey);
return await cipher.decrypt(encKey, userId);
}
}
@@ -718,6 +719,8 @@ export class CipherService implements CipherServiceAbstraction {
response: ListResponse<CipherResponse>,
organizationId: string,
): Promise<CipherView[]> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (response?.data == null || response.data.length < 1) {
return [];
}
@@ -726,7 +729,7 @@ export class CipherService implements CipherServiceAbstraction {
const key = await this.keyService.getOrgKey(organizationId);
const decCiphers: CipherView[] = await Promise.all(
ciphers.map(async (cipher) => {
return await cipher.decrypt(key);
return await cipher.decrypt(key, userId);
}),
);

View File

@@ -6,6 +6,7 @@ import { concatMap, firstValueFrom, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import {
@@ -15,7 +16,7 @@ import {
} from "@bitwarden/common/models/export";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { KeyService } from "@bitwarden/key-management";
@@ -167,6 +168,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
private async parseFolders(
data: BitwardenUnEncryptedIndividualJsonExport | BitwardenEncryptedIndividualJsonExport,
): Promise<Map<string, number>> | null {
const userId: UserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (data.folders == null) {
return null;
}
@@ -178,7 +181,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
if (data.encrypted) {
const folder = FolderWithIdExport.toDomain(f);
if (folder != null) {
folderView = await folder.decrypt();
folderView = await folder.decrypt(userId);
}
} else {
folderView = FolderWithIdExport.toView(f);

View File

@@ -175,7 +175,7 @@ export class DefaultUserAsymmetricKeysRegenerationService
}
try {
const cipherView = await cipher.decrypt(userKey);
const cipherView = await cipher.decrypt(userKey, userId);
if (cipherView.decryptionFailure) {
this.logService.error(