1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-02 09:43:29 +00:00

Migrate create and edit operations to use SDK for ciphers

This commit is contained in:
Nik Gilmore
2025-12-10 15:19:47 -08:00
parent f89c9b0f84
commit 04e455696e
10 changed files with 205 additions and 33 deletions

View File

@@ -963,6 +963,7 @@ export default class MainBackground {
this.logService,
this.cipherEncryptionService,
this.messagingService,
this.sdkService,
);
this.folderService = new FolderService(
this.keyService,

View File

@@ -787,6 +787,7 @@ export class ServiceContainer {
this.logService,
this.cipherEncryptionService,
this.messagingService,
this.sdkService,
);
this.cipherArchiveService = new DefaultCipherArchiveService(

View File

@@ -1510,8 +1510,7 @@ export class VaultComponent<C extends CipherViewLike> 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",

View File

@@ -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({

View File

@@ -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,

View File

@@ -110,9 +110,11 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* @returns A promise that resolves to the created cipher
*/
abstract createWithServer(
{ cipher, encryptedFor }: EncryptionContext,
cipherView: CipherView,
userId: UserId,
orgAdmin?: boolean,
): Promise<Cipher>;
): Promise<CipherView>;
/**
* Update a cipher with the server
* @param cipher The cipher to update
@@ -122,10 +124,10 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* @returns A promise that resolves to the updated cipher
*/
abstract updateWithServer(
{ cipher, encryptedFor }: EncryptionContext,
cipherView: CipherView,
userId: UserId,
orgAdmin?: boolean,
isNotClone?: boolean,
): Promise<Cipher>;
): Promise<CipherView>;
/**
* Move a cipher to an organization by re-encrypting its keys with the organization's key.

View File

@@ -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
*

View File

@@ -145,6 +145,7 @@ describe("Cipher Service", () => {
logService,
cipherEncryptionService,
messageSender,
sdkService,
);
encryptionContext = { cipher: new Cipher(cipherData), encryptedFor: userId };

View File

@@ -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<Record<CipherId, LocalData>> {
@@ -885,6 +888,51 @@ export class CipherService implements CipherServiceAbstraction {
}
async createWithServer(
cipherView: CipherView,
userId: UserId,
orgAdmin?: boolean,
): Promise<CipherView> {
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<CipherView | void> {
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<Cipher> {
@@ -911,6 +959,55 @@ export class CipherService implements CipherServiceAbstraction {
}
async updateWithServer(
cipherView: CipherView,
userId: UserId,
orgAdmin?: boolean,
): Promise<CipherView> {
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<CipherView> {
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<Cipher> {
@@ -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);

View File

@@ -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);