diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 1bd47186914..8ea050a6562 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -963,6 +963,7 @@ export default class MainBackground { this.logService, this.cipherEncryptionService, this.messagingService, + this.sdkService, ); this.folderService = new FolderService( this.keyService, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 83c64c61423..e75faf89a9a 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -787,6 +787,7 @@ export class ServiceContainer { this.logService, this.cipherEncryptionService, this.messagingService, + this.sdkService, ); this.cipherArchiveService = new DefaultCipherArchiveService( diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index b0685a028df..7a0e63e3299 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1510,8 +1510,7 @@ export class VaultComponent implements OnInit, OnDestr const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipherFullView = await this.cipherService.getFullCipherView(cipher); cipherFullView.favorite = !cipherFullView.favorite; - const encryptedCipher = await this.cipherService.encrypt(cipherFullView, activeUserId); - await this.cipherService.updateWithServer(encryptedCipher); + await this.cipherService.updateWithServer(cipherFullView, activeUserId); this.toastService.showToast({ variant: "success", diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index c2d4440b449..614e2bfccce 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -604,6 +604,7 @@ const safeProviders: SafeProvider[] = [ logService: LogService, cipherEncryptionService: CipherEncryptionService, messagingService: MessagingServiceAbstraction, + sdkService: SdkService, ) => new CipherService( keyService, @@ -620,6 +621,7 @@ const safeProviders: SafeProvider[] = [ logService, cipherEncryptionService, messagingService, + sdkService, ), deps: [ KeyService, @@ -636,6 +638,7 @@ const safeProviders: SafeProvider[] = [ LogService, CipherEncryptionService, MessagingServiceAbstraction, + SdkService, ], }), safeProvider({ diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 371081a89d9..31a188be88f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -64,6 +64,7 @@ export enum FeatureFlag { RiskInsightsForPremium = "pm-23904-risk-insights-for-premium", VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders", BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", + SdkCipherOperations = "use_sdk_cipher_operations", // TODO: Create & use a real feature flag. /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -123,6 +124,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.RiskInsightsForPremium]: FALSE, [FeatureFlag.VaultLoadingSkeletons]: FALSE, [FeatureFlag.BrowserPremiumSpotlight]: FALSE, + [FeatureFlag.SdkCipherOperations]: FALSE, /* Auth */ [FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE, diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index b95d9023a7c..870f353e311 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -110,9 +110,11 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + ): Promise; + /** * Update a cipher with the server * @param cipher The cipher to update @@ -122,10 +124,10 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + ): Promise; /** * Move a cipher to an organization by re-encrypting its keys with the organization's key. diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index db360f7f991..9e0f8a5c4fa 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -1,7 +1,12 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { asUuid, uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { ItemView } from "@bitwarden/common/vault/models/view/item.view"; -import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; +import { + CipherCreateRequest, + CipherEditRequest, + CipherViewType, + CipherView as SdkCipherView, +} from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; @@ -328,6 +333,75 @@ export class CipherView implements View, InitializerMetadata { return cipherView; } + /** + * Maps CipherView to SdkCipherView + * + * @returns {SdkCipherView} The SDK cipher view object + */ + toSdkCreateCipherRequest(): CipherCreateRequest { + const sdkCipherCreateRequest: CipherCreateRequest = { + organizationId: this.organizationId ? asUuid(this.organizationId) : undefined, + folderId: this.folderId ? asUuid(this.folderId) : undefined, + name: this.name ?? "", + notes: this.notes, + favorite: this.favorite ?? false, + reprompt: this.reprompt ?? CipherRepromptType.None, + fields: this.fields?.map((f) => f.toSdkFieldView()), + type: this.getSdkCipherViewType(), + }; + + return sdkCipherCreateRequest; + } + + /** + * Maps CipherView to SdkCipherView + * + * @returns {SdkCipherView} The SDK cipher view object + */ + toSdkUpdateCipherRequest(): CipherEditRequest { + const sdkCipherEditRequest: CipherEditRequest = { + id: this.id ? asUuid(this.id) : undefined, + organizationId: this.organizationId ? asUuid(this.organizationId) : undefined, + folderId: this.folderId ? asUuid(this.folderId) : undefined, + name: this.name ?? "", + notes: this.notes, + favorite: this.favorite ?? false, + reprompt: this.reprompt ?? CipherRepromptType.None, + fields: this.fields?.map((f) => f.toSdkFieldView()), + type: this.getSdkCipherViewType(), + revisionDate: this.revisionDate?.toISOString(), + archivedDate: this.archivedDate?.toISOString(), + attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), + key: this.key?.toSdk(), + }; + + return sdkCipherEditRequest; + } + + getSdkCipherViewType(): CipherViewType { + let viewType: CipherViewType; + switch (this.type) { + case CipherType.Card: + viewType = { card: this.card?.toSdkCardView() }; + break; + case CipherType.Identity: + viewType = { identity: this.identity?.toSdkIdentityView() }; + break; + case CipherType.Login: + viewType = { login: this.login?.toSdkLoginView() }; + break; + case CipherType.SecureNote: + viewType = { secureNote: this.secureNote?.toSdkSecureNoteView() }; + break; + case CipherType.SshKey: + viewType = { sshKey: this.sshKey?.toSdkSshKeyView() }; + break; + default: + break; + } + return viewType; + } + /** * Maps CipherView to SdkCipherView * diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 50823807fcf..82deb967aa4 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -145,6 +145,7 @@ describe("Cipher Service", () => { logService, cipherEncryptionService, messageSender, + sdkService, ); encryptionContext = { cipher: new Cipher(cipherData), encryptedFor: userId }; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 7eebe960a7f..99c4f74cf88 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,7 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { + catchError, combineLatest, + EMPTY, filter, firstValueFrom, map, @@ -17,7 +19,7 @@ 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. // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; -import { CipherListView } from "@bitwarden/sdk-internal"; +import { CipherListView, CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; import { ApiService } from "../../abstractions/api.service"; import { AccountService } from "../../auth/abstractions/account.service"; @@ -32,7 +34,7 @@ import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; -import { uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; +import { SdkService, uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; import { Utils } from "../../platform/misc/utils"; import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; @@ -120,6 +122,7 @@ export class CipherService implements CipherServiceAbstraction { private logService: LogService, private cipherEncryptionService: CipherEncryptionService, private messageSender: MessageSender, + private sdkService: SdkService, ) {} localData$(userId: UserId): Observable> { @@ -885,6 +888,51 @@ export class CipherService implements CipherServiceAbstraction { } async createWithServer( + cipherView: CipherView, + userId: UserId, + orgAdmin?: boolean, + ): Promise { + const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag( + FeatureFlag.SdkCipherOperations, + ); + + if (sdkCipherEncryptionEnabled) { + // return await this.createWithServer_sdk({ cipher, encryptedFor }, orgAdmin); + return (await this.createWithServer_sdk(cipherView, userId, orgAdmin)) || new CipherView(); + } else { + const encrypted = await this.encrypt(cipherView, userId); + const result = await this.createWithServer_legacy(encrypted, orgAdmin); + return await this.decrypt(result, userId); + } + } + + // TODO: Find a cleaner way to do this to replace the existing `createWitHServer` + // - should we do a new SErvice, or hijack existing service & change interfaces?? + private async createWithServer_sdk( + cipherView: CipherView, + userId: UserId, + orgAdmin?: boolean, + ): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + const sdkCreateRequest = cipherView.toSdkCreateCipherRequest(); + const result: SdkCipherView = await ref.value.vault().ciphers().create(sdkCreateRequest); + return CipherView.fromSdkCipherView(result); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to encrypt cipher: ${error}`); + return EMPTY; + }), + ), + ); + } + + private async createWithServer_legacy( { cipher, encryptedFor }: EncryptionContext, orgAdmin?: boolean, ): Promise { @@ -911,6 +959,55 @@ export class CipherService implements CipherServiceAbstraction { } async updateWithServer( + cipherView: CipherView, + userId: UserId, + orgAdmin?: boolean, + ): Promise { + const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag( + FeatureFlag.SdkCipherOperations, + ); + + if (sdkCipherEncryptionEnabled) { + return await this.updateWithServer_sdk(cipherView, userId, orgAdmin); + } else { + const encrypted = await this.encrypt(cipherView, userId); + const updatedCipher = await this.updateWithServer_legacy(encrypted, orgAdmin); + const updatedCipherView = this.decrypt(updatedCipher, userId); + return updatedCipherView; + } + } + + async updateWithServer_sdk( + cipher: CipherView, + userId: UserId, + orgAdmin?: boolean, + ): Promise { + return firstValueFrom( + this.sdkService.userClient$(userId).pipe( + map(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + const sdkUpdateRequest = cipher.toSdkUpdateCipherRequest(); + let result: SdkCipherView; + if (orgAdmin) { + // TODO: Need to expose ciphers admin client in SDK + result = await ref.value.vault().ciphers().edit(sdkUpdateRequest); + } else { + result = await ref.value.vault().ciphers().edit(sdkUpdateRequest); + } + return CipherView.fromSdkCipherView(result); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to encrypt cipher: ${error}`); + return EMPTY; + }), + ), + ); + } + + async updateWithServer_legacy( { cipher, encryptedFor }: EncryptionContext, orgAdmin?: boolean, ): Promise { @@ -1101,8 +1198,7 @@ export class CipherService implements CipherServiceAbstraction { //in order to keep item and it's attachments with the same encryption level if (cipher.key != null && !cipherKeyEncryptionEnabled) { const model = await this.decrypt(cipher, userId); - const reEncrypted = await this.encrypt(model, userId); - await this.updateWithServer(reEncrypted); + await this.updateWithServer(model, userId); } const encFileName = await this.encryptService.encryptString(filename, cipherEncKey); diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 59c583f980b..7ca71b6000d 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -37,9 +37,7 @@ export class DefaultCipherFormService implements CipherFormService { // Creating a new cipher if (cipher.id == null || cipher.id === "") { - const encrypted = await this.cipherService.encrypt(cipher, activeUserId); - savedCipher = await this.cipherService.createWithServer(encrypted, config.admin); - return await this.cipherService.decrypt(savedCipher, activeUserId); + return await this.cipherService.createWithServer(cipher, activeUserId, config.admin); } if (config.originalCipher == null) { @@ -66,35 +64,30 @@ export class DefaultCipherFormService implements CipherFormService { ); // If the collectionIds are the same, update the cipher normally } else if (isSetEqual(originalCollectionIds, newCollectionIds)) { - const encrypted = await this.cipherService.encrypt( + const savedCipherView = await this.cipherService.updateWithServer( cipher, activeUserId, - null, - null, - config.originalCipher, + config.admin, ); - savedCipher = await this.cipherService.updateWithServer(encrypted, config.admin); + // Temporary + savedCipher = await this.cipherService + .encrypt(savedCipherView, activeUserId) + .then((res) => res.cipher); } else { - const encrypted = await this.cipherService.encrypt( - cipher, - activeUserId, - null, - null, - config.originalCipher, - ); - const encryptedCipher = encrypted.cipher; - // Updating a cipher with collection changes is not supported with a single request currently // First update the cipher with the original collectionIds - encryptedCipher.collectionIds = config.originalCipher.collectionIds; - await this.cipherService.updateWithServer( - encrypted, + cipher.collectionIds = config.originalCipher.collectionIds; + const newCipher = await this.cipherService.updateWithServer( + cipher, + activeUserId, config.admin || originalCollectionIds.size === 0, ); // Then save the new collection changes separately - encryptedCipher.collectionIds = cipher.collectionIds; + newCipher.collectionIds = cipher.collectionIds; + // TODO: Remove after migrating all SDK ops + const { cipher: encryptedCipher } = await this.cipherService.encrypt(newCipher, activeUserId); if (config.admin || originalCollectionIds.size === 0) { // When using an admin config or the cipher was unassigned, update collections as an admin savedCipher = await this.cipherService.saveCollectionsWithServerAdmin(encryptedCipher);