mirror of
https://github.com/bitwarden/browser
synced 2026-02-26 17:43:22 +00:00
Merge branch 'vault/pm-27632/sdk-cipher-ops' into vault/pm-30303/sdk-cipher-ops-delete
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { CartResponse } from "@bitwarden/common/billing/models/response/cart.response";
|
||||
import { StorageResponse } from "@bitwarden/common/billing/models/response/storage.response";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { Cart } from "@bitwarden/pricing";
|
||||
import {
|
||||
BitwardenSubscription,
|
||||
Storage,
|
||||
SubscriptionStatus,
|
||||
SubscriptionStatuses,
|
||||
} from "@bitwarden/subscription";
|
||||
|
||||
export class BitwardenSubscriptionResponse extends BaseResponse {
|
||||
status: SubscriptionStatus;
|
||||
cart: Cart;
|
||||
storage: Storage;
|
||||
cancelAt?: Date;
|
||||
canceled?: Date;
|
||||
nextCharge?: Date;
|
||||
suspension?: Date;
|
||||
gracePeriod?: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
const status = this.getResponseProperty("Status");
|
||||
if (
|
||||
status !== SubscriptionStatuses.Incomplete &&
|
||||
status !== SubscriptionStatuses.IncompleteExpired &&
|
||||
status !== SubscriptionStatuses.Trialing &&
|
||||
status !== SubscriptionStatuses.Active &&
|
||||
status !== SubscriptionStatuses.PastDue &&
|
||||
status !== SubscriptionStatuses.Canceled &&
|
||||
status !== SubscriptionStatuses.Unpaid
|
||||
) {
|
||||
throw new Error(`Failed to parse invalid subscription status: ${status}`);
|
||||
}
|
||||
this.status = status;
|
||||
|
||||
this.cart = new CartResponse(this.getResponseProperty("Cart"));
|
||||
this.storage = new StorageResponse(this.getResponseProperty("Storage"));
|
||||
|
||||
const suspension = this.getResponseProperty("Suspension");
|
||||
if (suspension) {
|
||||
this.suspension = new Date(suspension);
|
||||
}
|
||||
|
||||
const gracePeriod = this.getResponseProperty("GracePeriod");
|
||||
if (gracePeriod) {
|
||||
this.gracePeriod = gracePeriod;
|
||||
}
|
||||
|
||||
const nextCharge = this.getResponseProperty("NextCharge");
|
||||
if (nextCharge) {
|
||||
this.nextCharge = new Date(nextCharge);
|
||||
}
|
||||
|
||||
const cancelAt = this.getResponseProperty("CancelAt");
|
||||
if (cancelAt) {
|
||||
this.cancelAt = new Date(cancelAt);
|
||||
}
|
||||
|
||||
const canceled = this.getResponseProperty("Canceled");
|
||||
if (canceled) {
|
||||
this.canceled = new Date(canceled);
|
||||
}
|
||||
}
|
||||
|
||||
toDomain = (): BitwardenSubscription => {
|
||||
switch (this.status) {
|
||||
case SubscriptionStatuses.Incomplete:
|
||||
case SubscriptionStatuses.IncompleteExpired:
|
||||
case SubscriptionStatuses.PastDue:
|
||||
case SubscriptionStatuses.Unpaid: {
|
||||
return {
|
||||
cart: this.cart,
|
||||
storage: this.storage,
|
||||
status: this.status,
|
||||
suspension: this.suspension!,
|
||||
gracePeriod: this.gracePeriod!,
|
||||
};
|
||||
}
|
||||
case SubscriptionStatuses.Trialing:
|
||||
case SubscriptionStatuses.Active: {
|
||||
return {
|
||||
cart: this.cart,
|
||||
storage: this.storage,
|
||||
status: this.status,
|
||||
nextCharge: this.nextCharge!,
|
||||
cancelAt: this.cancelAt,
|
||||
};
|
||||
}
|
||||
case SubscriptionStatuses.Canceled: {
|
||||
return {
|
||||
cart: this.cart,
|
||||
storage: this.storage,
|
||||
status: this.status,
|
||||
canceled: this.canceled!,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
97
libs/common/src/billing/models/response/cart.response.ts
Normal file
97
libs/common/src/billing/models/response/cart.response.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
SubscriptionCadence,
|
||||
SubscriptionCadenceIds,
|
||||
} from "@bitwarden/common/billing/types/subscription-pricing-tier";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { Cart, CartItem, Discount } from "@bitwarden/pricing";
|
||||
|
||||
import { DiscountResponse } from "./discount.response";
|
||||
|
||||
export class CartItemResponse extends BaseResponse implements CartItem {
|
||||
translationKey: string;
|
||||
quantity: number;
|
||||
cost: number;
|
||||
discount?: Discount;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.translationKey = this.getResponseProperty("TranslationKey");
|
||||
this.quantity = this.getResponseProperty("Quantity");
|
||||
this.cost = this.getResponseProperty("Cost");
|
||||
const discount = this.getResponseProperty("Discount");
|
||||
if (discount) {
|
||||
this.discount = discount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordManagerCartItemResponse extends BaseResponse {
|
||||
seats: CartItem;
|
||||
additionalStorage?: CartItem;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.seats = new CartItemResponse(this.getResponseProperty("Seats"));
|
||||
const additionalStorage = this.getResponseProperty("AdditionalStorage");
|
||||
if (additionalStorage) {
|
||||
this.additionalStorage = new CartItemResponse(additionalStorage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SecretsManagerCartItemResponse extends BaseResponse {
|
||||
seats: CartItem;
|
||||
additionalServiceAccounts?: CartItem;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.seats = new CartItemResponse(this.getResponseProperty("Seats"));
|
||||
const additionalServiceAccounts = this.getResponseProperty("AdditionalServiceAccounts");
|
||||
if (additionalServiceAccounts) {
|
||||
this.additionalServiceAccounts = new CartItemResponse(additionalServiceAccounts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CartResponse extends BaseResponse implements Cart {
|
||||
passwordManager: {
|
||||
seats: CartItem;
|
||||
additionalStorage?: CartItem;
|
||||
};
|
||||
secretsManager?: {
|
||||
seats: CartItem;
|
||||
additionalServiceAccounts?: CartItem;
|
||||
};
|
||||
cadence: SubscriptionCadence;
|
||||
discount?: Discount;
|
||||
estimatedTax: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.passwordManager = new PasswordManagerCartItemResponse(
|
||||
this.getResponseProperty("PasswordManager"),
|
||||
);
|
||||
|
||||
const secretsManager = this.getResponseProperty("SecretsManager");
|
||||
if (secretsManager) {
|
||||
this.secretsManager = new SecretsManagerCartItemResponse(secretsManager);
|
||||
}
|
||||
|
||||
const cadence = this.getResponseProperty("Cadence");
|
||||
if (cadence !== SubscriptionCadenceIds.Annually && cadence !== SubscriptionCadenceIds.Monthly) {
|
||||
throw new Error(`Failed to parse invalid cadence: ${cadence}`);
|
||||
}
|
||||
this.cadence = cadence;
|
||||
|
||||
const discount = this.getResponseProperty("Discount");
|
||||
if (discount) {
|
||||
this.discount = new DiscountResponse(discount);
|
||||
}
|
||||
|
||||
this.estimatedTax = this.getResponseProperty("EstimatedTax");
|
||||
}
|
||||
}
|
||||
18
libs/common/src/billing/models/response/discount.response.ts
Normal file
18
libs/common/src/billing/models/response/discount.response.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { Discount, DiscountType, DiscountTypes } from "@bitwarden/pricing";
|
||||
|
||||
export class DiscountResponse extends BaseResponse implements Discount {
|
||||
type: DiscountType;
|
||||
value: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
const type = this.getResponseProperty("Type");
|
||||
if (type !== DiscountTypes.AmountOff && type !== DiscountTypes.PercentOff) {
|
||||
throw new Error(`Failed to parse invalid discount type: ${type}`);
|
||||
}
|
||||
this.type = type;
|
||||
this.value = this.getResponseProperty("Value");
|
||||
}
|
||||
}
|
||||
16
libs/common/src/billing/models/response/storage.response.ts
Normal file
16
libs/common/src/billing/models/response/storage.response.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { Storage } from "@bitwarden/subscription";
|
||||
|
||||
export class StorageResponse extends BaseResponse implements Storage {
|
||||
available: number;
|
||||
used: number;
|
||||
readableUsed: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.available = this.getResponseProperty("Available");
|
||||
this.used = this.getResponseProperty("Used");
|
||||
this.readableUsed = this.getResponseProperty("ReadableUsed");
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ export enum FeatureFlag {
|
||||
PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog",
|
||||
PM26462_Milestone_3 = "pm-26462-milestone-3",
|
||||
PM23341_Milestone_2 = "pm-23341-milestone-2",
|
||||
PM29594_UpdateIndividualSubscriptionPage = "pm-29594-update-individual-subscription-page",
|
||||
PM29593_PremiumToOrganizationUpgrade = "pm-29593-premium-to-organization-upgrade",
|
||||
|
||||
/* Key Management */
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
@@ -54,7 +56,6 @@ export enum FeatureFlag {
|
||||
/* DIRT */
|
||||
EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike",
|
||||
PhishingDetection = "phishing-detection",
|
||||
PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab",
|
||||
|
||||
/* Vault */
|
||||
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
|
||||
@@ -117,7 +118,6 @@ export const DefaultFeatureFlagValue = {
|
||||
/* DIRT */
|
||||
[FeatureFlag.EventManagementForDataDogAndCrowdStrike]: FALSE,
|
||||
[FeatureFlag.PhishingDetection]: FALSE,
|
||||
[FeatureFlag.PM22887_RiskInsightsActivityTab]: FALSE,
|
||||
|
||||
/* Vault */
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
@@ -141,6 +141,8 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE,
|
||||
[FeatureFlag.PM26462_Milestone_3]: FALSE,
|
||||
[FeatureFlag.PM23341_Milestone_2]: FALSE,
|
||||
[FeatureFlag.PM29594_UpdateIndividualSubscriptionPage]: FALSE,
|
||||
[FeatureFlag.PM29593_PremiumToOrganizationUpgrade]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
|
||||
@@ -30,7 +30,7 @@ export class DefaultKeyGenerationService implements KeyGenerationService {
|
||||
): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }> {
|
||||
if (salt == null) {
|
||||
const bytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
salt = Utils.fromBufferToUtf8(bytes);
|
||||
salt = Utils.fromBufferToUtf8(bytes.buffer as ArrayBuffer);
|
||||
}
|
||||
const material = await this.cryptoFunctionService.aesGenerateKey(bitLength);
|
||||
const key = await this.cryptoFunctionService.hkdf(material, salt, purpose, 64, "sha256");
|
||||
|
||||
@@ -330,6 +330,7 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new PaymentResponse(r);
|
||||
}
|
||||
|
||||
// TODO: Remove with deletion of pm-29594-update-individual-subscription-page
|
||||
postReinstatePremium(): Promise<any> {
|
||||
return this.send("POST", "/accounts/reinstate-premium", null, true, false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendResponse } from "../response/send.response";
|
||||
|
||||
import { SendFileData } from "./send-file.data";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { mockContainerService, mockEnc } from "../../../../../spec";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendAccessResponse } from "../response/send-access.response";
|
||||
|
||||
import { SendAccess } from "./send-access";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { EncString } from "../../../../key-management/crypto/models/enc-string";
|
||||
import Domain from "../../../../platform/models/domain/domain-base";
|
||||
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendAccessResponse } from "../response/send-access.response";
|
||||
import { SendAccessView } from "../view/send-access.view";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { EncryptService } from "../../../../key-management/crypto/abstractions/e
|
||||
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "../../../../platform/services/container.service";
|
||||
import { UserKey } from "../../../../types/key";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendData } from "../data/send.data";
|
||||
|
||||
import { Send } from "./send";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { EncString } from "../../../../key-management/crypto/models/enc-string";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
import Domain from "../../../../platform/models/domain/domain-base";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendData } from "../data/send.data";
|
||||
import { SendView } from "../view/send.view";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendFileApi } from "../api/send-file.api";
|
||||
import { SendTextApi } from "../api/send-text.api";
|
||||
import { Send } from "../domain/send";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendFileApi } from "../api/send-file.api";
|
||||
import { SendTextApi } from "../api/send-text.api";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendFileApi } from "../api/send-file.api";
|
||||
import { SendTextApi } from "../api/send-text.api";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { View } from "../../../../models/view/view";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { SendAccess } from "../domain/send-access";
|
||||
|
||||
import { SendFileView } from "./send-file.view";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { View } from "../../../../models/view/view";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { DeepJsonify } from "../../../../types/deep-jsonify";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendType } from "../../types/send-type";
|
||||
import { Send } from "../domain/send";
|
||||
|
||||
import { SendFileView } from "./send-file.view";
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
FileUploadService,
|
||||
} from "../../../platform/abstractions/file-upload/file-upload.service";
|
||||
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
|
||||
import { SendType } from "../enums/send-type";
|
||||
import { SendData } from "../models/data/send.data";
|
||||
import { Send } from "../models/domain/send";
|
||||
import { SendAccessRequest } from "../models/request/send-access.request";
|
||||
@@ -16,6 +15,7 @@ import { SendFileDownloadDataResponse } from "../models/response/send-file-downl
|
||||
import { SendFileUploadDataResponse } from "../models/response/send-file-upload-data.response";
|
||||
import { SendResponse } from "../models/response/send.response";
|
||||
import { SendAccessView } from "../models/view/send-access.view";
|
||||
import { SendType } from "../types/send-type";
|
||||
|
||||
import { SendApiService as SendApiServiceAbstraction } from "./send-api.service.abstraction";
|
||||
import { InternalSendService } from "./send.service.abstraction";
|
||||
|
||||
@@ -24,13 +24,13 @@ import { ContainerService } from "../../../platform/services/container.service";
|
||||
import { SelfHostedEnvironment } from "../../../platform/services/default-environment.service";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
import { SendType } from "../enums/send-type";
|
||||
import { SendFileApi } from "../models/api/send-file.api";
|
||||
import { SendTextApi } from "../models/api/send-text.api";
|
||||
import { SendFileData } from "../models/data/send-file.data";
|
||||
import { SendTextData } from "../models/data/send-text.data";
|
||||
import { SendData } from "../models/data/send.data";
|
||||
import { SendView } from "../models/view/send.view";
|
||||
import { SendType } from "../types/send-type";
|
||||
|
||||
import { SEND_USER_DECRYPTED, SEND_USER_ENCRYPTED } from "./key-definitions";
|
||||
import { SendStateProvider } from "./send-state.provider";
|
||||
|
||||
@@ -16,7 +16,6 @@ import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey } from "../../../types/key";
|
||||
import { SendType } from "../enums/send-type";
|
||||
import { SendData } from "../models/data/send.data";
|
||||
import { Send } from "../models/domain/send";
|
||||
import { SendFile } from "../models/domain/send-file";
|
||||
@@ -24,6 +23,7 @@ import { SendText } from "../models/domain/send-text";
|
||||
import { SendWithIdRequest } from "../models/request/send-with-id.request";
|
||||
import { SendView } from "../models/view/send.view";
|
||||
import { SEND_KDF_ITERATIONS } from "../send-kdf";
|
||||
import { SendType } from "../types/send-type";
|
||||
|
||||
import { SendStateProvider } from "./send-state.provider.abstraction";
|
||||
import { InternalSendService as InternalSendServiceAbstraction } from "./send.service.abstraction";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EncString } from "../../../../key-management/crypto/models/enc-string";
|
||||
import { SendType } from "../../enums/send-type";
|
||||
import { SendTextApi } from "../../models/api/send-text.api";
|
||||
import { SendTextData } from "../../models/data/send-text.data";
|
||||
import { SendData } from "../../models/data/send.data";
|
||||
import { Send } from "../../models/domain/send";
|
||||
import { SendView } from "../../models/view/send.view";
|
||||
import { SendType } from "../../types/send-type";
|
||||
|
||||
export function testSendViewData(id: string, name: string) {
|
||||
const data = new SendView({} as any);
|
||||
|
||||
7
libs/common/src/tools/send/types/send-filter-type.ts
Normal file
7
libs/common/src/tools/send/types/send-filter-type.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const SendFilterType = Object.freeze({
|
||||
All: "all",
|
||||
Text: "text",
|
||||
File: "file",
|
||||
} as const);
|
||||
|
||||
export type SendFilterType = (typeof SendFilterType)[keyof typeof SendFilterType];
|
||||
@@ -3,12 +3,14 @@ import { Observable } from "rxjs";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
|
||||
export abstract class CipherArchiveService {
|
||||
abstract hasArchiveFlagEnabled$: Observable<boolean>;
|
||||
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
||||
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
||||
abstract userHasPremium$(userId: UserId): Observable<boolean>;
|
||||
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData>;
|
||||
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData>;
|
||||
abstract showSubscriptionEndedMessaging$(userId: UserId): Observable<boolean>;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ export abstract class CipherEncryptionService {
|
||||
*/
|
||||
abstract encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined>;
|
||||
|
||||
/**
|
||||
* Encrypts multiple ciphers using the SDK for the given userId.
|
||||
*
|
||||
* @param models The cipher views to encrypt
|
||||
* @param userId The user ID to initialize the SDK client with
|
||||
*
|
||||
* @returns A promise that resolves to an array of encryption contexts
|
||||
*/
|
||||
abstract encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]>;
|
||||
|
||||
/**
|
||||
* Move the cipher to the specified organization by re-encrypting its keys with the organization's key.
|
||||
* The cipher.organizationId will be updated to the new organizationId.
|
||||
|
||||
@@ -50,6 +50,15 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
keyForCipherKeyDecryption?: SymmetricCryptoKey,
|
||||
originalCipher?: Cipher,
|
||||
): Promise<EncryptionContext>;
|
||||
/**
|
||||
* Encrypts multiple ciphers for the given user.
|
||||
*
|
||||
* @param models The cipher views to encrypt
|
||||
* @param userId The user ID to encrypt for
|
||||
*
|
||||
* @returns A promise that resolves to an array of encryption contexts
|
||||
*/
|
||||
abstract encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]>;
|
||||
abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]>;
|
||||
abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise<Field>;
|
||||
abstract get(id: string, userId: UserId): Promise<Cipher>;
|
||||
|
||||
@@ -114,6 +114,10 @@ export class CipherView implements View, InitializerMetadata {
|
||||
return this.item?.subTitle;
|
||||
}
|
||||
|
||||
get canBeArchived(): boolean {
|
||||
return !this.isDeleted && !this.isArchived;
|
||||
}
|
||||
|
||||
get hasPasswordHistory(): boolean {
|
||||
return this.passwordHistory && this.passwordHistory.length > 0;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,24 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
|
||||
const sdkEncryptionEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM22136_SdkCipherEncryption,
|
||||
);
|
||||
|
||||
if (sdkEncryptionEnabled) {
|
||||
return await this.cipherEncryptionService.encryptMany(models, userId);
|
||||
}
|
||||
|
||||
// Fallback to sequential encryption if SDK disabled
|
||||
const results: EncryptionContext[] = [];
|
||||
for (const model of models) {
|
||||
const result = await this.encrypt(model, userId);
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async encryptAttachments(
|
||||
attachmentsModel: AttachmentView[],
|
||||
key: SymmetricCryptoKey,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* include structuredClone in test environment.
|
||||
* @jest-environment ../../../../shared/test.environment.ts
|
||||
*/
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom, BehaviorSubject } from "rxjs";
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { CipherArchiveService } from "../abstractions/cipher-archive.service";
|
||||
import { CipherData } from "../models/data/cipher.data";
|
||||
|
||||
export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
constructor(
|
||||
@@ -84,15 +85,17 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
);
|
||||
}
|
||||
|
||||
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData> {
|
||||
const request = new CipherBulkArchiveRequest(Array.isArray(ids) ? ids : [ids]);
|
||||
const r = await this.apiService.send("PUT", "/ciphers/archive", request, true, true);
|
||||
const response = new ListResponse(r, CipherResponse);
|
||||
|
||||
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
// prevent mutating ciphers$ state
|
||||
const localCiphers = structuredClone(currentCiphers);
|
||||
|
||||
for (const cipher of response.data) {
|
||||
const localCipher = currentCiphers[cipher.id as CipherId];
|
||||
const localCipher = localCiphers[cipher.id as CipherId];
|
||||
|
||||
if (localCipher == null) {
|
||||
continue;
|
||||
@@ -102,18 +105,21 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
localCipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
await this.cipherService.upsert(Object.values(currentCiphers), userId);
|
||||
await this.cipherService.upsert(Object.values(localCiphers), userId);
|
||||
return response.data[0];
|
||||
}
|
||||
|
||||
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData> {
|
||||
const request = new CipherBulkUnarchiveRequest(Array.isArray(ids) ? ids : [ids]);
|
||||
const r = await this.apiService.send("PUT", "/ciphers/unarchive", request, true, true);
|
||||
const response = new ListResponse(r, CipherResponse);
|
||||
|
||||
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
// prevent mutating ciphers$ state
|
||||
const localCiphers = structuredClone(currentCiphers);
|
||||
|
||||
for (const cipher of response.data) {
|
||||
const localCipher = currentCiphers[cipher.id as CipherId];
|
||||
const localCipher = localCiphers[cipher.id as CipherId];
|
||||
|
||||
if (localCipher == null) {
|
||||
continue;
|
||||
@@ -123,6 +129,7 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
localCipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
await this.cipherService.upsert(Object.values(currentCiphers), userId);
|
||||
await this.cipherService.upsert(Object.values(localCiphers), userId);
|
||||
return response.data[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,68 @@ describe("DefaultCipherEncryptionService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptMany", () => {
|
||||
it("should encrypt multiple ciphers", async () => {
|
||||
const cipherView2 = new CipherView(cipherObj);
|
||||
cipherView2.name = "test-name-2";
|
||||
const cipherView3 = new CipherView(cipherObj);
|
||||
cipherView3.name = "test-name-3";
|
||||
|
||||
const ciphers = [cipherViewObj, cipherView2, cipherView3];
|
||||
|
||||
const expectedCipher1: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name-1",
|
||||
} as unknown as Cipher;
|
||||
|
||||
const expectedCipher2: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name-2",
|
||||
} as unknown as Cipher;
|
||||
|
||||
const expectedCipher3: Cipher = {
|
||||
id: cipherId as string,
|
||||
type: CipherType.Login,
|
||||
name: "encrypted-name-3",
|
||||
} as unknown as Cipher;
|
||||
|
||||
mockSdkClient.vault().ciphers().encrypt.mockReturnValue({
|
||||
cipher: sdkCipher,
|
||||
encryptedFor: userId,
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(Cipher, "fromSdkCipher")
|
||||
.mockReturnValueOnce(expectedCipher1)
|
||||
.mockReturnValueOnce(expectedCipher2)
|
||||
.mockReturnValueOnce(expectedCipher3);
|
||||
|
||||
const results = await cipherEncryptionService.encryptMany(ciphers, userId);
|
||||
|
||||
expect(results).toBeDefined();
|
||||
expect(results.length).toBe(3);
|
||||
expect(results[0].cipher).toEqual(expectedCipher1);
|
||||
expect(results[1].cipher).toEqual(expectedCipher2);
|
||||
expect(results[2].cipher).toEqual(expectedCipher3);
|
||||
|
||||
expect(mockSdkClient.vault().ciphers().encrypt).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(results[0].encryptedFor).toBe(userId);
|
||||
expect(results[1].encryptedFor).toBe(userId);
|
||||
expect(results[2].encryptedFor).toBe(userId);
|
||||
});
|
||||
|
||||
it("should handle empty array", async () => {
|
||||
const results = await cipherEncryptionService.encryptMany([], userId);
|
||||
|
||||
expect(results).toBeDefined();
|
||||
expect(results.length).toBe(0);
|
||||
expect(mockSdkClient.vault().ciphers().encrypt).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("encryptCipherForRotation", () => {
|
||||
it("should call the sdk method to encrypt the cipher with a new key for rotation", async () => {
|
||||
mockSdkClient.vault().ciphers().encrypt_cipher_for_rotation.mockReturnValue({
|
||||
|
||||
@@ -51,6 +51,44 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService {
|
||||
);
|
||||
}
|
||||
|
||||
async encryptMany(models: CipherView[], userId: UserId): Promise<EncryptionContext[]> {
|
||||
if (!models || models.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
map((sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
|
||||
const results: EncryptionContext[] = [];
|
||||
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-30580
|
||||
// Replace this loop with a native SDK encryptMany method for better performance.
|
||||
for (const model of models) {
|
||||
const sdkCipherView = this.toSdkCipherView(model, ref.value);
|
||||
const encryptionContext = ref.value.vault().ciphers().encrypt(sdkCipherView);
|
||||
|
||||
results.push({
|
||||
cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!,
|
||||
encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to encrypt ciphers in batch: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async moveToOrganization(
|
||||
model: CipherView,
|
||||
organizationId: OrganizationId,
|
||||
|
||||
@@ -250,6 +250,38 @@ describe("DefaultCipherRiskService", () => {
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter out deleted Login ciphers", async () => {
|
||||
const mockClient = sdkService.simulate.userLogin(mockUserId);
|
||||
const mockCipherRiskClient = mockClient.vault.mockDeep().cipher_risk.mockDeep();
|
||||
mockCipherRiskClient.compute_risk.mockResolvedValue([]);
|
||||
|
||||
const activeCipher = new CipherView();
|
||||
activeCipher.id = mockCipherId1;
|
||||
activeCipher.type = CipherType.Login;
|
||||
activeCipher.login = new LoginView();
|
||||
activeCipher.login.password = "password1";
|
||||
activeCipher.deletedDate = undefined;
|
||||
|
||||
const deletedCipher = new CipherView();
|
||||
deletedCipher.id = mockCipherId2;
|
||||
deletedCipher.type = CipherType.Login;
|
||||
deletedCipher.login = new LoginView();
|
||||
deletedCipher.login.password = "password2";
|
||||
deletedCipher.deletedDate = new Date();
|
||||
|
||||
await cipherRiskService.computeRiskForCiphers([activeCipher, deletedCipher], mockUserId);
|
||||
|
||||
expect(mockCipherRiskClient.compute_risk).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
id: expect.anything(),
|
||||
password: "password1",
|
||||
}),
|
||||
],
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPasswordReuseMap", () => {
|
||||
@@ -284,6 +316,41 @@ describe("DefaultCipherRiskService", () => {
|
||||
]);
|
||||
expect(result).toEqual(mockReuseMap);
|
||||
});
|
||||
|
||||
it("should exclude deleted ciphers when building password reuse map", async () => {
|
||||
const mockClient = sdkService.simulate.userLogin(mockUserId);
|
||||
const mockCipherRiskClient = mockClient.vault.mockDeep().cipher_risk.mockDeep();
|
||||
|
||||
const mockReuseMap = {
|
||||
password1: 1,
|
||||
};
|
||||
|
||||
mockCipherRiskClient.password_reuse_map.mockReturnValue(mockReuseMap);
|
||||
|
||||
const activeCipher = new CipherView();
|
||||
activeCipher.id = mockCipherId1;
|
||||
activeCipher.type = CipherType.Login;
|
||||
activeCipher.login = new LoginView();
|
||||
activeCipher.login.password = "password1";
|
||||
activeCipher.deletedDate = undefined;
|
||||
|
||||
const deletedCipherWithSamePassword = new CipherView();
|
||||
deletedCipherWithSamePassword.id = mockCipherId2;
|
||||
deletedCipherWithSamePassword.type = CipherType.Login;
|
||||
deletedCipherWithSamePassword.login = new LoginView();
|
||||
deletedCipherWithSamePassword.login.password = "password1";
|
||||
deletedCipherWithSamePassword.deletedDate = new Date();
|
||||
|
||||
const result = await cipherRiskService.buildPasswordReuseMap(
|
||||
[activeCipher, deletedCipherWithSamePassword],
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(mockCipherRiskClient.password_reuse_map).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ password: "password1" }),
|
||||
]);
|
||||
expect(result).toEqual(mockReuseMap);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeCipherRiskForUser", () => {
|
||||
|
||||
@@ -71,7 +71,6 @@ export class DefaultCipherRiskService implements CipherRiskServiceAbstraction {
|
||||
passwordMap,
|
||||
checkExposed,
|
||||
});
|
||||
|
||||
return results[0];
|
||||
}
|
||||
|
||||
@@ -103,7 +102,8 @@ export class DefaultCipherRiskService implements CipherRiskServiceAbstraction {
|
||||
return (
|
||||
cipher.type === CipherType.Login &&
|
||||
cipher.login?.password != null &&
|
||||
cipher.login.password !== ""
|
||||
cipher.login.password !== "" &&
|
||||
!cipher.isDeleted
|
||||
);
|
||||
})
|
||||
.map(
|
||||
|
||||
Reference in New Issue
Block a user