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

[PM-21001] Move platform code to new encrypt service interface (#14544)

* Move platform code to new encrypt service interface

* Fix tests

* Fix tests

* Fix cli build
This commit is contained in:
Bernd Schoolmann
2025-05-20 19:45:40 +02:00
committed by GitHub
parent 23506b0bc1
commit d93f547cfb
12 changed files with 47 additions and 107 deletions

View File

@@ -46,22 +46,18 @@ describe("LocalBackedSessionStorage", () => {
it("returns a decrypted value when one is stored in local storage", async () => { it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted"); const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString; localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test"); const result = await sut.get("test");
// FIXME: Remove when updating file. Eslint update // FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
encrypted,
sessionKey,
"browser-session-key",
),
expect(result).toEqual("decrypted"); expect(result).toEqual("decrypted");
}); });
it("caches the decrypted value when one is stored in local storage", async () => { it("caches the decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted"); const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString; localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
await sut.get("test"); await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted"); expect(sut["cache"]["test"]).toEqual("decrypted");
}); });
@@ -69,22 +65,18 @@ describe("LocalBackedSessionStorage", () => {
it("returns a decrypted value when one is stored in local storage", async () => { it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted"); const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString; localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test"); const result = await sut.get("test");
// FIXME: Remove when updating file. Eslint update // FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
encrypted,
sessionKey,
"browser-session-key",
),
expect(result).toEqual("decrypted"); expect(result).toEqual("decrypted");
}); });
it("caches the decrypted value when one is stored in local storage", async () => { it("caches the decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted"); const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString; localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
await sut.get("test"); await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted"); expect(sut["cache"]["test"]).toEqual("decrypted");
}); });
@@ -104,7 +96,7 @@ describe("LocalBackedSessionStorage", () => {
it("returns true when the key is in local storage", async () => { it("returns true when the key is in local storage", async () => {
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.has("test"); const result = await sut.has("test");
expect(result).toBe(true); expect(result).toBe(true);
}); });
@@ -119,7 +111,7 @@ describe("LocalBackedSessionStorage", () => {
async (nullish) => { async (nullish) => {
localStorage.internalStore["session_test"] = nullish; localStorage.internalStore["session_test"] = nullish;
await expect(sut.has("test")).resolves.toBe(false); await expect(sut.has("test")).resolves.toBe(false);
expect(encryptService.decryptToUtf8).not.toHaveBeenCalled(); expect(encryptService.decryptString).not.toHaveBeenCalled();
}, },
); );
}); });
@@ -127,7 +119,7 @@ describe("LocalBackedSessionStorage", () => {
describe("save", () => { describe("save", () => {
const encString = makeEncString("encrypted"); const encString = makeEncString("encrypted");
beforeEach(() => { beforeEach(() => {
encryptService.encrypt.mockResolvedValue(encString); encryptService.encryptString.mockResolvedValue(encString);
}); });
it("logs a warning when saving the same value twice and in a dev environment", async () => { it("logs a warning when saving the same value twice and in a dev environment", async () => {
@@ -157,7 +149,10 @@ describe("LocalBackedSessionStorage", () => {
it("encrypts and saves the value to local storage", async () => { it("encrypts and saves the value to local storage", async () => {
await sut.save("test", "value"); await sut.save("test", "value");
expect(encryptService.encrypt).toHaveBeenCalledWith(JSON.stringify("value"), sessionKey); expect(encryptService.encryptString).toHaveBeenCalledWith(
JSON.stringify("value"),
sessionKey,
);
expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString); expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString);
}); });

View File

@@ -118,11 +118,7 @@ export class LocalBackedSessionStorageService
return null; return null;
} }
const valueJson = await this.encryptService.decryptToUtf8( const valueJson = await this.encryptService.decryptString(new EncString(local), encKey);
new EncString(local),
encKey,
"browser-session-key",
);
if (valueJson == null) { if (valueJson == null) {
// error with decryption, value is lost, delete state and start over // error with decryption, value is lost, delete state and start over
await this.localStorage.remove(this.sessionStorageKey(key)); await this.localStorage.remove(this.sessionStorageKey(key));
@@ -139,7 +135,10 @@ export class LocalBackedSessionStorageService
} }
const valueJson = JSON.stringify(value); const valueJson = JSON.stringify(value);
const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get()); const encValue = await this.encryptService.encryptString(
valueJson,
await this.sessionKey.get(),
);
await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString); await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString);
} }

View File

@@ -47,7 +47,7 @@ export abstract class DownloadCommand {
try { try {
const encBuf = await EncArrayBuffer.fromResponse(response); const encBuf = await EncArrayBuffer.fromResponse(response);
const decBuf = await this.encryptService.decryptToBytes(encBuf, key); const decBuf = await this.encryptService.decryptFileData(encBuf, key);
if (process.env.BW_SERVE === "true") { if (process.env.BW_SERVE === "true") {
const res = new FileResponse(Buffer.from(decBuf), fileName); const res = new FileResponse(Buffer.from(decBuf), fileName);
return Response.success(res); return Response.success(res);

View File

@@ -195,7 +195,7 @@ export class EditCommand {
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
); );
const request = new CollectionRequest(); const request = new CollectionRequest();
request.name = (await this.encryptService.encrypt(req.name, orgKey)).encryptedString; request.name = (await this.encryptService.encryptString(req.name, orgKey)).encryptedString;
request.externalId = req.externalId; request.externalId = req.externalId;
request.groups = groups; request.groups = groups;
request.users = users; request.users = users;

View File

@@ -453,10 +453,9 @@ export class GetCommand extends DownloadCommand {
const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id); const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id);
const decCollection = new CollectionView(response); const decCollection = new CollectionView(response);
decCollection.name = await this.encryptService.decryptToUtf8( decCollection.name = await this.encryptService.decryptString(
new EncString(response.name), new EncString(response.name),
orgKey, orgKey,
`orgkey-${options.organizationId}`,
); );
const groups = const groups =
response.groups == null response.groups == null

View File

@@ -61,7 +61,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
if (sessionKey == null) { if (sessionKey == null) {
throw new Error("No session key available."); throw new Error("No session key available.");
} }
const encValue = await this.encryptService.encryptToBytes( const encValue = await this.encryptService.encryptFileData(
Utils.fromB64ToArray(plainValue), Utils.fromB64ToArray(plainValue),
sessionKey, sessionKey,
); );
@@ -80,7 +80,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
} }
const encBuf = EncArrayBuffer.fromB64(encValue); const encBuf = EncArrayBuffer.fromB64(encValue);
const decValue = await this.encryptService.decryptToBytes(encBuf, sessionKey); const decValue = await this.encryptService.decryptFileData(encBuf, sessionKey);
if (decValue == null) { if (decValue == null) {
this.logService.info("Failed to decrypt."); this.logService.info("Failed to decrypt.");
return null; return null;

View File

@@ -110,7 +110,7 @@ export class ElectronKeyService extends DefaultKeyService {
// Set a key half if it doesn't exist // Set a key half if it doesn't exist
const keyBytes = await this.cryptoFunctionService.randomBytes(32); const keyBytes = await this.cryptoFunctionService.randomBytes(32);
clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString; clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
const encKey = await this.encryptService.encrypt(clientKeyHalf, userKey); const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey);
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
} }

View File

@@ -123,9 +123,6 @@ const mockCryptoService = () => {
encryptService.decryptString encryptService.decryptString
.calledWith(expect.any(EncString), expect.anything()) .calledWith(expect.any(EncString), expect.anything())
.mockResolvedValue("DECRYPTED_STRING"); .mockResolvedValue("DECRYPTED_STRING");
encryptService.decryptToUtf8
.calledWith(expect.any(EncString), expect.anything(), expect.anything())
.mockResolvedValue("DECRYPTED_STRING");
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);

View File

@@ -51,11 +51,6 @@ describe("DefaultvNextCollectionService", () => {
.mockImplementation((encString, key) => .mockImplementation((encString, key) =>
Promise.resolve(encString.data.replace("ENC_", "DEC_")), Promise.resolve(encString.data.replace("ENC_", "DEC_")),
); );
encryptService.decryptToUtf8
.calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey), expect.any(String))
.mockImplementation((encString, key) =>
Promise.resolve(encString.data.replace("ENC_", "DEC_")),
);
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
@@ -109,15 +104,13 @@ describe("DefaultvNextCollectionService", () => {
// Assert that the correct org keys were used for each encrypted string // Assert that the correct org keys were used for each encrypted string
// This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged // This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect(encryptService.decryptString).toHaveBeenCalledWith(
expect.objectContaining(new EncString(collection1.name)), expect.objectContaining(new EncString(collection1.name)),
orgKey1, orgKey1,
expect.any(String),
); );
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect(encryptService.decryptString).toHaveBeenCalledWith(
expect.objectContaining(new EncString(collection2.name)), expect.objectContaining(new EncString(collection2.name)),
orgKey2, orgKey2,
expect.any(String),
); );
}); });

View File

@@ -2,7 +2,6 @@ import { mock, MockProxy } from "jest-mock-extended";
import { makeEncString, makeSymmetricCryptoKey } from "../../../../spec"; import { makeEncString, makeSymmetricCryptoKey } from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { Utils } from "../../misc/utils";
import Domain from "./domain-base"; import Domain from "./domain-base";
import { EncString } from "./enc-string"; import { EncString } from "./enc-string";
@@ -22,24 +21,13 @@ describe("DomainBase", () => {
}); });
function setUpCryptography() { function setUpCryptography() {
encryptService.encrypt.mockImplementation((value) => { encryptService.encryptString.mockImplementation((value) =>
let data: string; Promise.resolve(makeEncString(value)),
if (typeof value === "string") { );
data = value;
} else {
data = Utils.fromBufferToUtf8(value);
}
return Promise.resolve(makeEncString(data)); encryptService.decryptString.mockImplementation((value) => {
});
encryptService.decryptToUtf8.mockImplementation((value) => {
return Promise.resolve(value.data); return Promise.resolve(value.data);
}); });
encryptService.decryptToBytes.mockImplementation((value) => {
return Promise.resolve(value.dataBytes);
});
} }
describe("decryptWithKey", () => { describe("decryptWithKey", () => {
@@ -82,7 +70,7 @@ describe("DomainBase", () => {
const domain = new TestDomain(); const domain = new TestDomain();
domain.encToString = await encryptService.encrypt("string", key); domain.encToString = await encryptService.encryptString("string", key);
const decrypted = await domain["decryptObjWithKey"](["encToString"], key, encryptService); const decrypted = await domain["decryptObjWithKey"](["encToString"], key, encryptService);
@@ -96,8 +84,8 @@ describe("DomainBase", () => {
const domain = new TestDomain(); const domain = new TestDomain();
domain.encToString = await encryptService.encrypt("string", key); domain.encToString = await encryptService.encryptString("string", key);
domain.encString2 = await encryptService.encrypt("string2", key); domain.encString2 = await encryptService.encryptString("string2", key);
const decrypted = await domain["decryptObjWithKey"]( const decrypted = await domain["decryptObjWithKey"](
["encToString", "encString2"], ["encToString", "encString2"],

View File

@@ -7,7 +7,6 @@ import { EncryptService } from "../../../key-management/crypto/abstractions/encr
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { UserKey, OrgKey } from "../../../types/key"; import { UserKey, OrgKey } from "../../../types/key";
import { EncryptionType } from "../../enums"; import { EncryptionType } from "../../enums";
import { Utils } from "../../misc/utils";
import { ContainerService } from "../../services/container.service"; import { ContainerService } from "../../services/container.service";
import { EncString } from "./enc-string"; import { EncString } from "./enc-string";
@@ -87,7 +86,7 @@ describe("EncString", () => {
); );
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
encryptService.decryptToUtf8 encryptService.decryptString
.calledWith(encString, expect.anything()) .calledWith(encString, expect.anything())
.mockResolvedValue("decrypted"); .mockResolvedValue("decrypted");
@@ -106,7 +105,7 @@ describe("EncString", () => {
it("result should be cached", async () => { it("result should be cached", async () => {
const decrypted = await encString.decrypt(null); const decrypted = await encString.decrypt(null);
expect(encryptService.decryptToUtf8).toBeCalledTimes(1); expect(encryptService.decryptString).toBeCalledTimes(1);
expect(decrypted).toBe("decrypted"); expect(decrypted).toBe("decrypted");
}); });
@@ -118,24 +117,17 @@ describe("EncString", () => {
const keyService = mock<KeyService>(); const keyService = mock<KeyService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
encryptService.decryptToUtf8 encryptService.decryptString
.calledWith(encString, expect.anything()) .calledWith(encString, expect.anything())
.mockResolvedValue("decrypted"); .mockResolvedValue("decrypted");
function setupEncryption() { function setupEncryption() {
encryptService.encrypt.mockImplementation(async (data, key) => { encryptService.encryptString.mockImplementation(async (data, key) => {
if (typeof data === "string") {
return makeEncString(data); return makeEncString(data);
} else {
return makeEncString(Utils.fromBufferToUtf8(data));
}
}); });
encryptService.decryptToUtf8.mockImplementation(async (encString, key) => { encryptService.decryptString.mockImplementation(async (encString, key) => {
return encString.data; return encString.data;
}); });
encryptService.decryptToBytes.mockImplementation(async (encString, key) => {
return encString.dataBytes;
});
} }
beforeEach(() => { beforeEach(() => {
@@ -148,7 +140,7 @@ describe("EncString", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const key = new SymmetricCryptoKey(makeStaticByteArray(32));
await encString.decryptWithKey(key, encryptService); await encString.decryptWithKey(key, encryptService);
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key, "domain-withkey"); expect(encryptService.decryptString).toHaveBeenCalledWith(encString, key);
}); });
it("fails to decrypt when key is null", async () => { it("fails to decrypt when key is null", async () => {
@@ -169,7 +161,7 @@ describe("EncString", () => {
}); });
it("fails to decrypt when encryptService throws", async () => { it("fails to decrypt when encryptService throws", async () => {
encryptService.decryptToUtf8.mockRejectedValue("error"); encryptService.decryptString.mockRejectedValue("error");
const decrypted = await encString.decryptWithKey( const decrypted = await encString.decryptWithKey(
new SymmetricCryptoKey(makeStaticByteArray(32)), new SymmetricCryptoKey(makeStaticByteArray(32)),
@@ -330,7 +322,7 @@ describe("EncString", () => {
}); });
it("handles value it can't decrypt", async () => { it("handles value it can't decrypt", async () => {
encryptService.decryptToUtf8.mockRejectedValue("error"); encryptService.decryptString.mockRejectedValue("error");
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
@@ -350,7 +342,7 @@ describe("EncString", () => {
await encString.decrypt(null, key); await encString.decrypt(null, key);
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key, "provided-key"); expect(encryptService.decryptString).toHaveBeenCalledWith(encString, key);
}); });
it("gets an organization key if required", async () => { it("gets an organization key if required", async () => {
@@ -361,11 +353,7 @@ describe("EncString", () => {
await encString.decrypt("orgId", null); await encString.decrypt("orgId", null);
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId"); expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect(encryptService.decryptString).toHaveBeenCalledWith(encString, orgKey);
encString,
orgKey,
"domain-orgkey-orgId",
);
}); });
it("gets the user's decryption key if required", async () => { it("gets the user's decryption key if required", async () => {
@@ -376,11 +364,7 @@ describe("EncString", () => {
await encString.decrypt(null, null); await encString.decrypt(null, null);
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalledWith(); expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalledWith();
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( expect(encryptService.decryptString).toHaveBeenCalledWith(encString, userKey);
encString,
userKey,
"domain-withlegacysupport-masterkey",
);
}); });
}); });

View File

@@ -163,31 +163,16 @@ export class EncString implements Encrypted {
return this.decryptedValue; return this.decryptedValue;
} }
let decryptTrace = "provided-key";
try { try {
if (key == null) { if (key == null) {
key = await this.getKeyForDecryption(orgId); key = await this.getKeyForDecryption(orgId);
decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
if (orgId != null) {
decryptTrace = `domain-orgkey-${orgId}`;
} else {
const cryptoService = Utils.getContainerService().getKeyService();
decryptTrace =
(await cryptoService.getUserKey()) == null
? "domain-withlegacysupport-masterkey"
: "domain-withlegacysupport-userkey";
}
} }
if (key == null) { if (key == null) {
throw new Error("No key to decrypt EncString with orgId " + orgId); throw new Error("No key to decrypt EncString with orgId " + orgId);
} }
const encryptService = Utils.getContainerService().getEncryptService(); const encryptService = Utils.getContainerService().getEncryptService();
this.decryptedValue = await encryptService.decryptToUtf8( this.decryptedValue = await encryptService.decryptString(this, key);
this,
key,
decryptTrace == null ? context : `${decryptTrace}${context || ""}`,
);
// FIXME: Remove when updating file. Eslint update // FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) { } catch (e) {
@@ -206,7 +191,7 @@ export class EncString implements Encrypted {
throw new Error("No key to decrypt EncString"); throw new Error("No key to decrypt EncString");
} }
this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace); this.decryptedValue = await encryptService.decryptString(this, key);
// FIXME: Remove when updating file. Eslint update // FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) { } catch (e) {