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

[PM-12423] Migrate Cipher Decryption to Use SDK (#14206)

* Created mappings for client domain object to SDK

* Add abstract decrypt observable

* Added todo for future consideration

* Added implementation to cipher service

* Added adapter and unit tests

* Created cipher encryption abstraction and service

* Register cipher encryption service

* Added tests for the cipher encryption service

* changed signature

* Updated feature flag name

* added new function to be used for decrypting ciphers

* Added new encryptedKey field

* added new function to be used for decrypting ciphers

* Manually set fields

* Added encrypted key in attachment view

* Fixed test

* Updated references to use decrypt with feature flag

* Added dependency

* updated package.json

* lint fix

* fixed tests

* Fixed small mapping issues

* Fixed test

* Added function to decrypt fido2 key value

* Added function to decrypt fido2 key value and updated test

* updated to use sdk function without prociding the key

* updated localdata sdk type change

* decrypt attachment content using sdk

* Fixed dependency issues

* updated package.json

* Refactored service to handle getting decrypted buffer using the legacy and sdk implementations

* updated services and component to use refactored version

* Updated decryptCiphersWithSdk to use decryptManyLegacy for batch decryption, ensuring the SDK is only called once per batch

* Fixed merge conflicts

* Fixed merge conflicts

* Fixed merge conflicts

* Fixed lint issues

* Moved getDecryptedAttachmentBuffer to cipher service

* Moved getDecryptedAttachmentBuffer to cipher service

* ensure CipherView properties are null instead of undefined

* Fixed test

* ensure AttachmentView properties are null instead of undefined

* Linked ticket in comment

* removed unused orgKey
This commit is contained in:
SmithThe4th
2025-05-14 10:30:01 -04:00
committed by GitHub
parent 3e0cc7ca7f
commit ad3121f535
85 changed files with 2171 additions and 218 deletions

View File

@@ -29,7 +29,8 @@ 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 { perUserCache$ } from "../../vault/utils/observable-utilities";
import { filterOutNullish, perUserCache$ } from "../../vault/utils/observable-utilities";
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";
@@ -103,6 +104,7 @@ export class CipherService implements CipherServiceAbstraction {
private stateProvider: StateProvider,
private accountService: AccountService,
private logService: LogService,
private cipherEncryptionService: CipherEncryptionService,
) {}
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
@@ -424,13 +426,21 @@ export class CipherService implements CipherServiceAbstraction {
ciphers: Cipher[],
userId: UserId,
): Promise<[CipherView[], CipherView[]] | null> {
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
const decryptStartTime = new Date().getTime();
const decrypted = await this.decryptCiphersWithSdk(ciphers, userId);
this.logService.info(
`[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
);
// With SDK, failed ciphers are not returned
return [decrypted, []];
}
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 null;
}
// Group ciphers by orgId or under 'null' for the user's ciphers
const grouped = ciphers.reduce(
(agg, c) => {
@@ -440,7 +450,6 @@ export class CipherService implements CipherServiceAbstraction {
},
{} as Record<string, Cipher[]>,
);
const decryptStartTime = new Date().getTime();
const allCipherViews = (
await Promise.all(
@@ -464,7 +473,6 @@ export class CipherService implements CipherServiceAbstraction {
this.logService.info(
`[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
);
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
return allCipherViews.reduce(
(acc, c) => {
@@ -479,6 +487,21 @@ export class CipherService implements CipherServiceAbstraction {
);
}
/**
* 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 decrypt(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) {
const reindexRequired =
this.searchService != null &&
@@ -895,7 +918,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.decrypt(cipher, userId);
cipher = await this.encrypt(model, userId);
await this.updateWithServer(cipher);
}
@@ -1381,6 +1404,43 @@ export class CipherService implements CipherServiceAbstraction {
return encryptedCiphers;
}
async getDecryptedAttachmentBuffer(
cipherId: CipherId,
attachment: AttachmentView,
response: Response,
userId: UserId,
): Promise<Uint8Array> {
const useSdkDecryption = await this.configService.getFeatureFlag(
FeatureFlag.PM19941MigrateCipherDomainToSdk,
);
const cipherDomain = await firstValueFrom(
this.ciphers$(userId).pipe(map((ciphersData) => new Cipher(ciphersData[cipherId]))),
);
if (useSdkDecryption) {
const encryptedContent = await response.arrayBuffer();
return this.cipherEncryptionService.decryptAttachmentContent(
cipherDomain,
attachment,
new Uint8Array(encryptedContent),
userId,
);
}
const encBuf = await EncArrayBuffer.fromResponse(response);
const key =
attachment.key != null
? attachment.key
: await firstValueFrom(
this.keyService.orgKeys$(userId).pipe(
filterOutNullish(),
map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey),
),
);
return await this.encryptService.decryptFileData(encBuf, key);
}
/**
* @returns a SingleUserState
*/
@@ -1430,9 +1490,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.decrypt(originalCipher, userId);
model.passwordHistory = existingCipher.passwordHistory || [];
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
if (
@@ -1852,4 +1910,17 @@ 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 The decrypted ciphers.
* @private
*/
private async decryptCiphersWithSdk(ciphers: Cipher[], userId: UserId): Promise<CipherView[]> {
const decryptedViews = await this.cipherEncryptionService.decryptManyLegacy(ciphers, userId);
return decryptedViews.sort(this.getLocaleSortingFunction());
}
}