mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 05:30:01 +00:00
added new function to be used for decrypting ciphers
This commit is contained in:
@@ -15,6 +15,7 @@ import { EncryptService } from "../../key-management/crypto/abstractions/encrypt
|
||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "../../platform/abstractions/state.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
@@ -23,6 +24,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
||||
import { ContainerService } from "../../platform/services/container.service";
|
||||
import { CipherId, UserId } from "../../types/guid";
|
||||
import { CipherKey, OrgKey, UserKey } from "../../types/key";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType } from "../enums";
|
||||
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
|
||||
@@ -122,6 +124,8 @@ describe("Cipher Service", () => {
|
||||
const configService = mock<ConfigService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
const sdkService = mock<SdkService>();
|
||||
const cipherEncryptionService = mock<CipherEncryptionService>();
|
||||
|
||||
const userId = "TestUserId" as UserId;
|
||||
|
||||
@@ -148,6 +152,8 @@ describe("Cipher Service", () => {
|
||||
configService,
|
||||
stateProvider,
|
||||
accountService,
|
||||
sdkService,
|
||||
cipherEncryptionService,
|
||||
);
|
||||
|
||||
cipherObj = new Cipher(cipherData);
|
||||
@@ -470,4 +476,29 @@ describe("Cipher Service", () => {
|
||||
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptCipherWithSdkOrLegacy", () => {
|
||||
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(cipherObj));
|
||||
|
||||
const result = await cipherService.decryptCipherWithSdkOrLegacy(cipherObj, userId);
|
||||
|
||||
expect(result).toEqual(new CipherView(cipherObj));
|
||||
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipherObj, userId);
|
||||
});
|
||||
|
||||
it("should call legacy decrypt when feature flag is false", async () => {
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
|
||||
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
|
||||
jest.spyOn(cipherObj, "decrypt").mockResolvedValue(new CipherView(cipherObj));
|
||||
|
||||
const result = await cipherService.decryptCipherWithSdkOrLegacy(cipherObj, userId);
|
||||
|
||||
expect(result).toEqual(new CipherView(cipherObj));
|
||||
expect(cipherObj.decrypt).toHaveBeenCalledWith(mockUserKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { SearchService } from "../../abstractions/search.service";
|
||||
@@ -41,6 +40,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
||||
import { StateProvider } from "../../platform/state";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FieldType } from "../enums";
|
||||
@@ -113,6 +113,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private stateProvider: StateProvider,
|
||||
private accountService: AccountService,
|
||||
private sdkService: SdkService,
|
||||
private cipherEncryptionService: CipherEncryptionService,
|
||||
) {}
|
||||
|
||||
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
|
||||
@@ -158,23 +159,6 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link CipherServiceAbstraction.decrypt$}
|
||||
*/
|
||||
decrypt$(userId: UserId, cipher: Cipher): Observable<SdkCipherView> {
|
||||
return this.sdkService.userClient$(userId).pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK is undefined");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
|
||||
return ref.value.vault().ciphers().decrypt(cipher.toSdkCipher());
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async setDecryptedCipherCache(value: CipherView[], userId: UserId) {
|
||||
// Sometimes we might prematurely decrypt the vault and that will result in no ciphers
|
||||
// if we cache it then we may accidentally return it when it's not right, we'd rather try decryption again.
|
||||
@@ -448,55 +432,70 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
ciphers: Cipher[],
|
||||
userId: UserId,
|
||||
): Promise<[CipherView[], CipherView[]]> {
|
||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
||||
|
||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||
// return early if there are no keys to decrypt with
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
// Group ciphers by orgId or under 'null' for the user's ciphers
|
||||
const grouped = ciphers.reduce(
|
||||
(agg, c) => {
|
||||
agg[c.organizationId] ??= [];
|
||||
agg[c.organizationId].push(c);
|
||||
return agg;
|
||||
},
|
||||
{} as Record<string, Cipher[]>,
|
||||
);
|
||||
|
||||
const allCipherViews = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(async ([orgId, groupedCiphers]) => {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||
return await this.bulkEncryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||
);
|
||||
} else {
|
||||
return await this.encryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||
);
|
||||
}
|
||||
}),
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
|
||||
return this.decryptCiphersWithSdk(ciphers, userId);
|
||||
} else {
|
||||
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
|
||||
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
|
||||
// return early if there are no keys to decrypt with
|
||||
return [[], []];
|
||||
}
|
||||
// Group ciphers by orgId or under 'null' for the user's ciphers
|
||||
const grouped = ciphers.reduce(
|
||||
(agg, c) => {
|
||||
agg[c.organizationId] ??= [];
|
||||
agg[c.organizationId].push(c);
|
||||
return agg;
|
||||
},
|
||||
{} as Record<string, Cipher[]>,
|
||||
);
|
||||
const allCipherViews = (
|
||||
await Promise.all(
|
||||
Object.entries(grouped).map(async ([orgId, groupedCiphers]) => {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||
return await this.bulkEncryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||
);
|
||||
} else {
|
||||
return await this.encryptService.decryptItems(
|
||||
groupedCiphers,
|
||||
keys.orgKeys[orgId as OrganizationId] ?? keys.userKey,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
.flat()
|
||||
.sort(this.getLocaleSortingFunction());
|
||||
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
|
||||
return allCipherViews.reduce(
|
||||
(acc, c) => {
|
||||
if (c.decryptionFailure) {
|
||||
acc[1].push(c);
|
||||
} else {
|
||||
acc[0].push(c);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[[], []] as [CipherView[], CipherView[]],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
|
||||
return allCipherViews.reduce(
|
||||
(acc, c) => {
|
||||
if (c.decryptionFailure) {
|
||||
acc[1].push(c);
|
||||
} else {
|
||||
acc[0].push(c);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[[], []] as [CipherView[], CipherView[]],
|
||||
);
|
||||
/**
|
||||
* Decrypts a cipher using either the SDK or the legacy method based on the feature flag.
|
||||
* @param cipher The cipher to decrypt.
|
||||
* @param userId The user ID to use for decryption.
|
||||
* @returns A promise that resolves to the decrypted cipher view.
|
||||
*/
|
||||
async decryptCipherWithSdkOrLegacy(cipher: Cipher, userId: UserId): Promise<CipherView> {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
|
||||
return await this.cipherEncryptionService.decrypt(cipher, userId);
|
||||
} else {
|
||||
const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId);
|
||||
return await cipher.decrypt(encKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async reindexCiphers(userId: UserId) {
|
||||
@@ -910,7 +909,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
//then we rollback to using the user key as the main key of encryption of the item
|
||||
//in order to keep item and it's attachments with the same encryption level
|
||||
if (cipher.key != null && !cipherKeyEncryptionEnabled) {
|
||||
const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher, userId));
|
||||
const model = await this.decryptCipherWithSdkOrLegacy(cipher, userId);
|
||||
cipher = await this.encrypt(model, userId);
|
||||
await this.updateWithServer(cipher);
|
||||
}
|
||||
@@ -1441,9 +1440,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
originalCipher: Cipher,
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
const existingCipher = await originalCipher.decrypt(
|
||||
await this.getKeyForCipherKeyDecryption(originalCipher, userId),
|
||||
);
|
||||
const existingCipher = await this.decryptCipherWithSdkOrLegacy(originalCipher, userId);
|
||||
model.passwordHistory = existingCipher.passwordHistory || [];
|
||||
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
|
||||
if (
|
||||
@@ -1856,4 +1853,33 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
);
|
||||
return featureEnabled && meetsServerVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the provided ciphers using the SDK.
|
||||
* @param ciphers The ciphers to decrypt.
|
||||
* @param userId The user ID to use for decryption.
|
||||
* @returns A tuple containing the successful and failed decrypted ciphers.
|
||||
* @private
|
||||
*/
|
||||
private async decryptCiphersWithSdk(
|
||||
ciphers: Cipher[],
|
||||
userId: UserId,
|
||||
): Promise<[CipherView[], CipherView[]]> {
|
||||
const decryptedViews = await Promise.all(
|
||||
ciphers.map((cipher) => this.cipherEncryptionService.decrypt(cipher, userId)),
|
||||
);
|
||||
|
||||
const successful: CipherView[] = [];
|
||||
const failed: CipherView[] = [];
|
||||
|
||||
decryptedViews.forEach((view) => {
|
||||
if (view.decryptionFailure) {
|
||||
failed.push(view);
|
||||
} else {
|
||||
successful.push(view);
|
||||
}
|
||||
});
|
||||
|
||||
return [successful.sort(this.getLocaleSortingFunction()), failed];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user