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:
@@ -963,6 +963,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.cipherEncryptionService,
|
||||
this.messagingService,
|
||||
this.sdkService,
|
||||
);
|
||||
this.folderService = new FolderService(
|
||||
this.keyService,
|
||||
|
||||
@@ -787,6 +787,7 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
this.cipherEncryptionService,
|
||||
this.messagingService,
|
||||
this.sdkService,
|
||||
);
|
||||
|
||||
this.cipherArchiveService = new DefaultCipherArchiveService(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -145,6 +145,7 @@ describe("Cipher Service", () => {
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
messageSender,
|
||||
sdkService,
|
||||
);
|
||||
|
||||
encryptionContext = { cipher: new Cipher(cipherData), encryptedFor: userId };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user