1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

fixed merge conflict

This commit is contained in:
gbubemismith
2023-08-15 13:59:18 -04:00
711 changed files with 47498 additions and 11078 deletions

View File

@@ -243,6 +243,9 @@ export abstract class ApiService {
putRestoreManyCiphers: (
request: CipherBulkRestoreRequest
) => Promise<ListResponse<CipherResponse>>;
putRestoreManyCiphersAdmin: (
request: CipherBulkRestoreRequest
) => Promise<ListResponse<CipherResponse>>;
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.

View File

@@ -201,6 +201,17 @@ export abstract class OrganizationUserService {
request: OrganizationUserResetPasswordRequest
): Promise<void>;
/**
* Enable Secrets Manager for many users
* @param organizationId - Identifier for the organization the user belongs to
* @param ids - List of organization user identifiers to enable
* @return List of user ids, including both those that were successfully enabled and those that had an error
*/
abstract putOrganizationUserBulkEnableSecretsManager(
organizationId: string,
ids: string[]
): Promise<void>;
/**
* Delete an organization user
* @param organizationId - Identifier for the organization the user belongs to

View File

@@ -14,6 +14,7 @@ export class OrganizationUserResponse extends BaseResponse {
accessSecretsManager: boolean;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
hasMasterPassword: boolean;
collections: SelectionReadOnlyResponse[] = [];
groups: string[] = [];
@@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
this.accessAll = this.getResponseProperty("AccessAll");
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
const collections = this.getResponseProperty("Collections");
if (collections != null) {

View File

@@ -3,9 +3,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
import { PaymentRequest } from "../../../billing/models/request/payment.request";
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
import { BillingResponse } from "../../../billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
import { PaymentResponse } from "../../../billing/models/response/payment.response";
@@ -16,7 +18,6 @@ import { StorageRequest } from "../../../models/request/storage.request";
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationApiKeyType } from "../../enums";
import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request";
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
@@ -25,6 +26,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
import { OrganizationResponse } from "../../models/response/organization.response";
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
export class OrganizationApiServiceAbstraction {
get: (id: string) => Promise<OrganizationResponse>;
@@ -37,7 +39,14 @@ export class OrganizationApiServiceAbstraction {
save: (id: string, request: OrganizationUpdateRequest) => Promise<OrganizationResponse>;
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
updateSubscription: (id: string, request: OrganizationSubscriptionUpdateRequest) => Promise<void>;
updatePasswordManagerSeats: (
id: string,
request: OrganizationSubscriptionUpdateRequest
) => Promise<void>;
updateSecretsManagerSubscription: (
id: string,
request: OrganizationSmSubscriptionUpdateRequest
) => Promise<void>;
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
@@ -60,8 +69,8 @@ export class OrganizationApiServiceAbstraction {
getSso: (id: string) => Promise<OrganizationSsoResponse>;
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
selfHostedSyncLicense: (id: string) => Promise<void>;
updateEnrollSecretsManager: (
subscribeToSecretsManager: (
id: string,
request: OrganizationEnrollSecretsManagerRequest
) => Promise<void>;
request: SecretsManagerSubscribeRequest
) => Promise<ProfileOrganizationResponse>;
}

View File

@@ -15,7 +15,8 @@ export function canAccessSettingsTab(org: Organization): boolean {
org.canManagePolicies ||
org.canManageSso ||
org.canManageScim ||
org.canAccessImportExport
org.canAccessImportExport ||
org.canManageDeviceApprovals
);
}
@@ -56,6 +57,12 @@ export function canAccessAdmin(i18nService: I18nService) {
);
}
export function canAccessImportExport(i18nService: I18nService) {
return map<Organization[], Organization[]>((orgs) =>
orgs.filter((org) => org.canAccessImportExport).sort(Utils.getSortFunction(i18nService, "name"))
);
}
/**
* Returns `true` if a user is a member of an organization (rather than only being a ProviderUser)
* @deprecated Use organizationService.memberOrganizations$ instead
@@ -85,6 +92,7 @@ export abstract class OrganizationService {
hasOrganizations: () => boolean;
}
export abstract class InternalOrganizationService extends OrganizationService {
export abstract class InternalOrganizationServiceAbstraction extends OrganizationService {
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
upsert: (OrganizationData: OrganizationData | OrganizationData[]) => Promise<void>;
}

View File

@@ -22,6 +22,7 @@ export class OrganizationData {
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
usePasswordManager: boolean;
useActivateAutofillPolicy: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -74,6 +75,7 @@ export class OrganizationData {
this.useCustomPermissions = response.useCustomPermissions;
this.useResetPassword = response.useResetPassword;
this.useSecretsManager = response.useSecretsManager;
this.usePasswordManager = response.usePasswordManager;
this.useActivateAutofillPolicy = response.useActivateAutofillPolicy;
this.selfHost = response.selfHost;
this.usersGetPremium = response.usersGetPremium;

View File

@@ -31,6 +31,7 @@ export class Organization {
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
usePasswordManager: boolean;
useActivateAutofillPolicy: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -87,6 +88,7 @@ export class Organization {
this.useCustomPermissions = obj.useCustomPermissions;
this.useResetPassword = obj.useResetPassword;
this.useSecretsManager = obj.useSecretsManager;
this.usePasswordManager = obj.usePasswordManager;
this.useActivateAutofillPolicy = obj.useActivateAutofillPolicy;
this.selfHost = obj.selfHost;
this.usersGetPremium = obj.usersGetPremium;
@@ -215,6 +217,10 @@ export class Organization {
return this.isAdmin || this.permissions.manageResetPassword;
}
get canManageDeviceApprovals() {
return (this.isAdmin || this.permissions.manageResetPassword) && this.useSso;
}
get isExemptFromPolicies() {
return this.canManagePolicies;
}

View File

@@ -23,4 +23,8 @@ export class OrganizationCreateRequest {
billingAddressState: string;
billingAddressPostalCode: string;
billingAddressCountry: string;
useSecretsManager: boolean;
additionalSmSeats: number;
additionalServiceAccounts: number;
}

View File

@@ -11,4 +11,8 @@ export class OrganizationUpgradeRequest {
billingAddressCountry: string;
billingAddressPostalCode: string;
keys: OrganizationKeysRequest;
useSecretsManager: boolean;
additionalSmSeats: number;
additionalServiceAccounts: number;
}

View File

@@ -1,3 +0,0 @@
export class OrganizationEnrollSecretsManagerRequest {
enabled: boolean;
}

View File

@@ -13,6 +13,7 @@ export class OrganizationResponse extends BaseResponse {
businessTaxNumber: string;
billingEmail: string;
plan: PlanResponse;
secretsManagerPlan: PlanResponse;
planType: PlanType;
seats: number;
maxAutoscaleSeats: number;
@@ -27,6 +28,11 @@ export class OrganizationResponse extends BaseResponse {
useResetPassword: boolean;
useSecretsManager: boolean;
hasPublicAndPrivateKeys: boolean;
usePasswordManager: boolean;
smSeats?: number;
smServiceAccounts?: number;
maxAutoscaleSmSeats?: number;
maxAutoscaleSmServiceAccounts?: number;
constructor(response: any) {
super(response);
@@ -39,8 +45,14 @@ export class OrganizationResponse extends BaseResponse {
this.businessCountry = this.getResponseProperty("BusinessCountry");
this.businessTaxNumber = this.getResponseProperty("BusinessTaxNumber");
this.billingEmail = this.getResponseProperty("BillingEmail");
const plan = this.getResponseProperty("Plan");
this.plan = plan == null ? null : new PlanResponse(plan);
const secretsManagerPlan = this.getResponseProperty("SecretsManagerPlan");
this.secretsManagerPlan =
secretsManagerPlan == null ? null : new PlanResponse(secretsManagerPlan);
this.planType = this.getResponseProperty("PlanType");
this.seats = this.getResponseProperty("Seats");
this.maxAutoscaleSeats = this.getResponseProperty("MaxAutoscaleSeats");
@@ -55,5 +67,10 @@ export class OrganizationResponse extends BaseResponse {
this.useResetPassword = this.getResponseProperty("UseResetPassword");
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
this.hasPublicAndPrivateKeys = this.getResponseProperty("HasPublicAndPrivateKeys");
this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
this.smSeats = this.getResponseProperty("SmSeats");
this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts");
this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats");
this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts");
}
}

View File

@@ -19,6 +19,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
useCustomPermissions: boolean;
useResetPassword: boolean;
useSecretsManager: boolean;
usePasswordManager: boolean;
useActivateAutofillPolicy: boolean;
selfHost: boolean;
usersGetPremium: boolean;
@@ -65,6 +66,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
this.useResetPassword = this.getResponseProperty("UseResetPassword");
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
this.usePasswordManager = this.getResponseProperty("UsePasswordManager");
this.useActivateAutofillPolicy = this.getResponseProperty("UseActivateAutofillPolicy");
this.selfHost = this.getResponseProperty("SelfHost");
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");

View File

@@ -4,9 +4,11 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request";
import { PaymentRequest } from "../../../billing/models/request/payment.request";
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
import { BillingResponse } from "../../../billing/models/response/billing.response";
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
import { PaymentResponse } from "../../../billing/models/response/payment.response";
@@ -19,7 +21,6 @@ import { ListResponse } from "../../../models/response/list.response";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
import { OrganizationApiKeyType } from "../../enums";
import { OrganizationEnrollSecretsManagerRequest } from "../../models/request/organization/organization-enroll-secrets-manager.request";
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
@@ -28,6 +29,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
import { OrganizationResponse } from "../../models/response/organization.response";
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
constructor(private apiService: ApiService, private syncService: SyncService) {}
@@ -120,7 +122,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
return new PaymentResponse(r);
}
async updateSubscription(
async updatePasswordManagerSeats(
id: string,
request: OrganizationSubscriptionUpdateRequest
): Promise<void> {
@@ -133,6 +135,19 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
);
}
async updateSecretsManagerSubscription(
id: string,
request: OrganizationSmSubscriptionUpdateRequest
): Promise<void> {
return this.apiService.send(
"POST",
"/organizations/" + id + "/sm-subscription",
request,
true,
false
);
}
async updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse> {
const r = await this.apiService.send(
"POST",
@@ -294,13 +309,17 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
);
}
async updateEnrollSecretsManager(id: string, request: OrganizationEnrollSecretsManagerRequest) {
await this.apiService.send(
async subscribeToSecretsManager(
id: string,
request: SecretsManagerSubscribeRequest
): Promise<ProfileOrganizationResponse> {
const r = await this.apiService.send(
"POST",
"/organizations/" + id + "/enroll-secrets-manager",
"/organizations/" + id + "/subscribe-secrets-manager",
request,
true,
true
);
return new ProfileOrganizationResponse(r);
}
}

View File

@@ -2,7 +2,7 @@ import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
import { StateService } from "../../../platform/abstractions/state.service";
import {
InternalOrganizationService as InternalOrganizationServiceAbstraction,
InternalOrganizationServiceAbstraction,
isMember,
} from "../../abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "../../models/data/organization.data";

View File

@@ -1,4 +1,4 @@
import { VerifyOTPRequest } from "../../auth/models/request/verify-otp.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
export abstract class UserVerificationApiServiceAbstraction {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;

View File

@@ -1,5 +1,5 @@
import { SecretVerificationRequest } from "../../auth/models/request/secret-verification.request";
import { Verification } from "../../types/verification";
import { Verification } from "../../../types/verification";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>(

View File

@@ -1,9 +1,9 @@
import { ApiService } from "../../abstractions/api.service";
import { UserVerificationService } from "../../abstractions/userVerification/userVerification.service.abstraction";
import { LogService } from "../../platform/abstractions/log.service";
import { Verification } from "../../types/verification";
import { AccountApiService } from "../abstractions/account-api.service";
import { InternalAccountService } from "../abstractions/account.service";
import { UserVerificationService } from "../abstractions/user-verification/user-verification.service.abstraction";
export class AccountApiServiceImplementation implements AccountApiService {
constructor(

View File

@@ -302,15 +302,18 @@ export class AuthService implements AuthServiceAbstraction {
(
await this.cryptoService.getKey()
).encKey,
pubKey.buffer
);
const encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
pubKey.buffer
pubKey
);
let encryptedMasterPassword = null;
if ((await this.stateService.getKeyHash()) != null) {
encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
pubKey
);
}
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterPassword.encryptedString,
encryptedMasterPassword?.encryptedString,
await this.appIdService.getAppId(),
requestApproved
);

View File

@@ -1,5 +1,5 @@
import { ApiService } from "../../../abstractions/api.service";
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
export class UserVerificationApiService implements UserVerificationApiServiceAbstraction {

View File

@@ -1,8 +1,8 @@
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../../abstractions/userVerification/userVerification.service.abstraction";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { Verification } from "../../../types/verification";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "../../enums/verification-type";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";

View File

@@ -0,0 +1,4 @@
export enum BitwardenProductType {
PasswordManager = 0,
SecretsManager = 1,
}

View File

@@ -2,3 +2,4 @@ export * from "./payment-method-type.enum";
export * from "./plan-sponsorship-type.enum";
export * from "./plan-type.enum";
export * from "./transaction-type.enum";
export * from "./bitwarden-product-type.enum";

View File

@@ -0,0 +1,21 @@
export class OrganizationSmSubscriptionUpdateRequest {
/**
* The number of seats to add or remove from the subscription.
*/
seatAdjustment: number;
/**
* The maximum number of seats that can be auto-scaled for the subscription.
*/
maxAutoscaleSeats?: number;
/**
* The number of additional service accounts to add or remove from the subscription.
*/
serviceAccountAdjustment: number;
/**
* The maximum number of additional service accounts that can be auto-scaled for the subscription.
*/
maxAutoscaleServiceAccounts?: number;
}

View File

@@ -1,3 +1,23 @@
export class OrganizationSubscriptionUpdateRequest {
constructor(public seatAdjustment: number, public maxAutoscaleSeats?: number) {}
/**
* The number of seats to add or remove from the subscription.
* Applies to both PM and SM request types.
*/
seatAdjustment: number;
/**
* The maximum number of seats that can be auto-scaled for the subscription.
* Applies to both PM and SM request types.
*/
maxAutoscaleSeats?: number;
/**
* Build a subscription update request for the Password Manager product type.
* @param seatAdjustment - The number of seats to add or remove from the subscription.
* @param maxAutoscaleSeats - The maximum number of seats that can be auto-scaled for the subscription.
*/
constructor(seatAdjustment: number, maxAutoscaleSeats?: number) {
this.seatAdjustment = seatAdjustment;
this.maxAutoscaleSeats = maxAutoscaleSeats;
}
}

View File

@@ -0,0 +1,4 @@
export class SecretsManagerSubscribeRequest {
additionalSmSeats: number;
additionalServiceAccounts: number;
}

View File

@@ -12,6 +12,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
expiration: string;
expirationWithoutGracePeriod: string;
secretsManagerBeta: boolean;
constructor(response: any) {
super(response);
@@ -26,5 +27,6 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
this.expiration = this.getResponseProperty("Expiration");
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta");
}
}

View File

@@ -1,10 +1,11 @@
import { ProductType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { PlanType } from "../../enums";
import { BitwardenProductType, PlanType } from "../../enums";
export class PlanResponse extends BaseResponse {
type: PlanType;
product: ProductType;
bitwardenProduct: BitwardenProductType;
name: string;
isAnnual: boolean;
nameLocalizationKey: string;
@@ -48,6 +49,15 @@ export class PlanResponse extends BaseResponse {
additionalStoragePricePerGb: number;
premiumAccessOptionPrice: number;
// SM only
additionalPricePerServiceAccount: number;
baseServiceAccount: number;
maxServiceAccount: number;
hasAdditionalServiceAccountOption: boolean;
maxProjects: number;
maxAdditionalServiceAccounts: number;
stripeServiceAccountPlanId: string;
constructor(response: any) {
super(response);
this.type = this.getResponseProperty("Type");
@@ -90,5 +100,18 @@ export class PlanResponse extends BaseResponse {
this.seatPrice = this.getResponseProperty("SeatPrice");
this.additionalStoragePricePerGb = this.getResponseProperty("AdditionalStoragePricePerGb");
this.premiumAccessOptionPrice = this.getResponseProperty("PremiumAccessOptionPrice");
this.bitwardenProduct = this.getResponseProperty("BitwardenProduct");
this.additionalPricePerServiceAccount = this.getResponseProperty(
"AdditionalPricePerServiceAccount"
);
this.baseServiceAccount = this.getResponseProperty("BaseServiceAccount");
this.maxServiceAccount = this.getResponseProperty("MaxServiceAccount");
this.hasAdditionalServiceAccountOption = this.getResponseProperty(
"HasAdditionalServiceAccountOption"
);
this.maxProjects = this.getResponseProperty("MaxProjects");
this.maxAdditionalServiceAccounts = this.getResponseProperty("MaxAdditionalServiceAccounts");
this.stripeServiceAccountPlanId = this.getResponseProperty("StripeServiceAccountPlanId");
}
}

View File

@@ -1,4 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
import { BitwardenProductType } from "../../enums";
export class SubscriptionResponse extends BaseResponse {
storageName: string;
@@ -62,6 +63,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
quantity: number;
interval: string;
sponsoredSubscriptionItem: boolean;
addonSubscriptionItem: boolean;
bitwardenProduct: BitwardenProductType;
constructor(response: any) {
super(response);
@@ -70,6 +73,8 @@ export class BillingSubscriptionItemResponse extends BaseResponse {
this.quantity = this.getResponseProperty("Quantity");
this.interval = this.getResponseProperty("Interval");
this.sponsoredSubscriptionItem = this.getResponseProperty("SponsoredSubscriptionItem");
this.addonSubscriptionItem = this.getResponseProperty("AddonSubscriptionItem");
this.bitwardenProduct = this.getResponseProperty("BitwardenProduct");
}
}

View File

@@ -3,4 +3,5 @@ export enum FeatureFlag {
DisplayLowKdfIterationWarningFlag = "display-kdf-iteration-warning",
Fido2VaultCredentials = "fido2-vault-credentials",
TrustedDeviceEncryption = "trusted-device-encryption",
SecretsManagerBilling = "sm-ga-billing",
}

View File

@@ -10,4 +10,5 @@ export abstract class ConfigServiceAbstraction {
getFeatureFlagBool: (key: FeatureFlag, defaultValue?: boolean) => Promise<boolean>;
getFeatureFlagString: (key: FeatureFlag, defaultValue?: string) => Promise<string>;
getFeatureFlagNumber: (key: FeatureFlag, defaultValue?: number) => Promise<number>;
getCloudRegion: (defaultValue?: string) => Promise<string>;
}

View File

@@ -4,67 +4,67 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class CryptoFunctionService {
pbkdf2: (
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
password: string | Uint8Array,
salt: string | Uint8Array,
algorithm: "sha256" | "sha512",
iterations: number
) => Promise<ArrayBuffer>;
) => Promise<Uint8Array>;
argon2: (
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
password: string | Uint8Array,
salt: string | Uint8Array,
iterations: number,
memory: number,
parallelism: number
) => Promise<ArrayBuffer>;
) => Promise<Uint8Array>;
hkdf: (
ikm: ArrayBuffer,
salt: string | ArrayBuffer,
info: string | ArrayBuffer,
ikm: Uint8Array,
salt: string | Uint8Array,
info: string | Uint8Array,
outputByteSize: number,
algorithm: "sha256" | "sha512"
) => Promise<ArrayBuffer>;
) => Promise<Uint8Array>;
hkdfExpand: (
prk: ArrayBuffer,
info: string | ArrayBuffer,
prk: Uint8Array,
info: string | Uint8Array,
outputByteSize: number,
algorithm: "sha256" | "sha512"
) => Promise<ArrayBuffer>;
) => Promise<Uint8Array>;
hash: (
value: string | ArrayBuffer,
value: string | Uint8Array,
algorithm: "sha1" | "sha256" | "sha512" | "md5"
) => Promise<ArrayBuffer>;
) => Promise<Uint8Array>;
hmac: (
value: ArrayBuffer,
key: ArrayBuffer,
value: Uint8Array,
key: Uint8Array,
algorithm: "sha1" | "sha256" | "sha512"
) => Promise<ArrayBuffer>;
compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise<boolean>;
) => Promise<Uint8Array>;
compare: (a: Uint8Array, b: Uint8Array) => Promise<boolean>;
hmacFast: (
value: ArrayBuffer | string,
key: ArrayBuffer | string,
value: Uint8Array | string,
key: Uint8Array | string,
algorithm: "sha1" | "sha256" | "sha512"
) => Promise<ArrayBuffer | string>;
compareFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise<boolean>;
aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
) => Promise<Uint8Array | string>;
compareFast: (a: Uint8Array | string, b: Uint8Array | string) => Promise<boolean>;
aesEncrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise<Uint8Array>;
aesDecryptFastParameters: (
data: string,
iv: string,
mac: string,
key: SymmetricCryptoKey
) => DecryptParameters<ArrayBuffer | string>;
aesDecryptFast: (parameters: DecryptParameters<ArrayBuffer | string>) => Promise<string>;
aesDecrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
) => DecryptParameters<Uint8Array | string>;
aesDecryptFast: (parameters: DecryptParameters<Uint8Array | string>) => Promise<string>;
aesDecrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise<Uint8Array>;
rsaEncrypt: (
data: ArrayBuffer,
publicKey: ArrayBuffer,
data: Uint8Array,
publicKey: Uint8Array,
algorithm: "sha1" | "sha256"
) => Promise<ArrayBuffer>;
) => Promise<Uint8Array>;
rsaDecrypt: (
data: ArrayBuffer,
privateKey: ArrayBuffer,
data: Uint8Array,
privateKey: Uint8Array,
algorithm: "sha1" | "sha256"
) => Promise<ArrayBuffer>;
rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise<ArrayBuffer>;
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>;
) => Promise<Uint8Array>;
rsaExtractPublicKey: (privateKey: Uint8Array) => Promise<Uint8Array>;
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[Uint8Array, Uint8Array]>;
randomBytes: (length: number) => Promise<CsprngArray>;
}

View File

@@ -22,9 +22,9 @@ export abstract class CryptoService {
getKeyHash: () => Promise<string>;
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<ArrayBuffer>;
getPrivateKey: () => Promise<ArrayBuffer>;
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
getPublicKey: () => Promise<Uint8Array>;
getPrivateKey: () => Promise<Uint8Array>;
getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise<string[]>;
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
@@ -63,7 +63,7 @@ export abstract class CryptoService {
kdf: KdfType,
kdfConfig: KdfConfig
) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: Uint8Array) => Promise<SymmetricCryptoKey>;
hashPassword: (
password: string,
key: SymmetricCryptoKey,
@@ -74,13 +74,13 @@ export abstract class CryptoService {
key: SymmetricCryptoKey,
encKey?: SymmetricCryptoKey
) => Promise<[SymmetricCryptoKey, EncString]>;
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncString>;
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise<EncString>;
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise<EncString>;
encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise<EncString>;
rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise<Uint8Array>;
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<Uint8Array>;
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise<Uint8Array>;
randomNumber: (min: number, max: number) => Promise<number>;
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
}

View File

@@ -6,13 +6,13 @@ import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class EncryptService {
abstract encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString>;
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
abstract encryptToBytes: (
plainValue: ArrayBuffer,
plainValue: Uint8Array,
key?: SymmetricCryptoKey
) => Promise<EncArrayBuffer>;
abstract decryptToUtf8: (encString: EncString, key: SymmetricCryptoKey) => Promise<string>;
abstract decryptToBytes: (encThing: Encrypted, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
abstract decryptToBytes: (encThing: Encrypted, key: SymmetricCryptoKey) => Promise<Uint8Array>;
abstract resolveLegacyKey: (key: SymmetricCryptoKey, encThing: Encrypted) => SymmetricCryptoKey;
abstract decryptItems: <T extends InitializerMetadata>(
items: Decryptable<T>[],

View File

@@ -17,12 +17,41 @@ export type PayPalConfig = {
buttonAction?: string;
};
export enum Region {
US = "US",
EU = "EU",
SelfHosted = "Self-hosted",
}
export enum RegionDomain {
US = "bitwarden.com",
EU = "bitwarden.eu",
USQA = "bitwarden.pw",
}
export abstract class EnvironmentService {
urls: Observable<void>;
usUrls: Urls;
euUrls: Urls;
selectedRegion?: Region;
initialized = true;
hasBaseUrl: () => boolean;
getNotificationsUrl: () => string;
getWebVaultUrl: () => string;
/**
* Retrieves the URL of the cloud web vault app.
*
* @returns {string} The URL of the cloud web vault app.
* @remarks Use this method only in views exclusive to self-host instances.
*/
getCloudWebVaultUrl: () => string;
/**
* Sets the URL of the cloud web vault app based on the region parameter.
*
* @param {Region} region - The region of the cloud web vault app.
*/
setCloudWebVaultUrl: (region: Region) => void;
getSendUrl: () => string;
getIconsUrl: () => string;
getApiUrl: () => string;
@@ -32,11 +61,8 @@ export abstract class EnvironmentService {
getScimUrl: () => string;
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
setRegion: (region: Region) => Promise<void>;
getUrls: () => Urls;
isCloud: () => boolean;
/**
* @remarks For desktop and browser use only.
* For web, use PlatformUtilsService.isSelfHost()
*/
isSelfHosted: () => boolean;
isEmpty: () => boolean;
}

View File

@@ -113,8 +113,8 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this, use PolicyService
*/
setDecryptedPolicies: (value: Policy[], options?: StorageOptions) => Promise<void>;
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<Uint8Array>;
setDecryptedPrivateKey: (value: Uint8Array, options?: StorageOptions) => Promise<void>;
getDecryptedProviderKeys: (options?: StorageOptions) => Promise<Map<string, SymmetricCryptoKey>>;
setDecryptedProviderKeys: (
value: Map<string, SymmetricCryptoKey>,
@@ -263,6 +263,8 @@ export abstract class StateService<T extends Account = Account> {
setEntityType: (value: string, options?: StorageOptions) => Promise<void>;
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
getRegion: (options?: StorageOptions) => Promise<string>;
setRegion: (value: string, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
@@ -329,8 +331,8 @@ export abstract class StateService<T extends Account = Account> {
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
getPublicKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
getPublicKey: (options?: StorageOptions) => Promise<Uint8Array>;
setPublicKey: (value: Uint8Array, options?: StorageOptions) => Promise<void>;
getRefreshToken: (options?: StorageOptions) => Promise<string>;
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
getRememberedEmail: (options?: StorageOptions) => Promise<string>;

View File

@@ -2,7 +2,7 @@ import { EncryptionType } from "../../enums";
export interface Encrypted {
encryptionType?: EncryptionType;
dataBytes: ArrayBuffer;
macBytes: ArrayBuffer;
ivBytes: ArrayBuffer;
dataBytes: Uint8Array;
macBytes: Uint8Array;
ivBytes: Uint8Array;
}

View File

@@ -358,4 +358,32 @@ describe("Utils Service", () => {
expect(actual.protocol).toBe("http:");
});
});
describe("daysRemaining", () => {
beforeAll(() => {
const now = new Date(2023, 9, 2, 10);
jest.spyOn(Date, "now").mockReturnValue(now.getTime());
});
afterAll(() => {
jest.restoreAllMocks();
});
it("should return 0 for equal dates", () => {
expect(Utils.daysRemaining(new Date(2023, 9, 2))).toBe(0);
expect(Utils.daysRemaining(new Date(2023, 9, 2, 12))).toBe(0);
});
it("should return 0 for dates in the past", () => {
expect(Utils.daysRemaining(new Date(2020, 5, 11))).toBe(0);
expect(Utils.daysRemaining(new Date(2023, 9, 1))).toBe(0);
});
it("should handle future dates", () => {
expect(Utils.daysRemaining(new Date(2023, 9, 3, 10))).toBe(1);
expect(Utils.daysRemaining(new Date(2023, 10, 12, 10))).toBe(41);
// leap year
expect(Utils.daysRemaining(new Date(2024, 9, 2, 10))).toBe(366);
});
});
});

View File

@@ -545,6 +545,16 @@ export class Utils {
return of(undefined).pipe(switchMap(() => generator()));
}
/**
* Return the number of days remaining before a target date arrives.
* Returns 0 if the day has already passed.
*/
static daysRemaining(targetDate: Date): number {
const diffTime = targetDate.getTime() - Date.now();
const msPerDay = 86400000;
return Math.max(0, Math.floor(diffTime / msPerDay));
}
private static isAppleMobile(win: Window) {
return (
win.navigator.userAgent.match(/iPhone/i) != null ||

View File

@@ -1,3 +1,5 @@
import { Region } from "../../abstractions/environment.service";
import {
EnvironmentServerConfigData,
ServerConfigData,
@@ -15,6 +17,7 @@ describe("ServerConfigData", () => {
url: "https://test.com",
},
environment: {
cloudRegion: Region.EU,
vault: "https://vault.com",
api: "https://api.com",
identity: "https://identity.com",

View File

@@ -1,5 +1,6 @@
import { Jsonify } from "type-fest";
import { Region } from "../../abstractions/environment.service";
import {
ServerConfigResponse,
ThirdPartyServerConfigResponse,
@@ -50,6 +51,7 @@ export class ThirdPartyServerConfigData {
}
export class EnvironmentServerConfigData {
cloudRegion: Region;
vault: string;
api: string;
identity: string;
@@ -57,6 +59,7 @@ export class EnvironmentServerConfigData {
sso: string;
constructor(response: Partial<EnvironmentServerConfigResponse>) {
this.cloudRegion = response.cloudRegion;
this.vault = response.vault;
this.api = response.api;
this.identity = response.identity;

View File

@@ -8,7 +8,7 @@ describe("AccountKeys", () => {
describe("toJSON", () => {
it("should serialize itself", () => {
const keys = new AccountKeys();
const buffer = makeStaticByteArray(64).buffer;
const buffer = makeStaticByteArray(64);
keys.publicKey = buffer;
const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString");
@@ -18,7 +18,7 @@ describe("AccountKeys", () => {
it("should serialize public key as a string", () => {
const keys = new AccountKeys();
keys.publicKey = Utils.fromByteStringToArray("hello").buffer;
keys.publicKey = Utils.fromByteStringToArray("hello");
const json = JSON.stringify(keys);
expect(json).toContain('"publicKey":"hello"');
});
@@ -29,7 +29,7 @@ describe("AccountKeys", () => {
const keys = AccountKeys.fromJSON({
publicKey: "hello",
});
expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello").buffer);
expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello"));
});
it("should deserialize cryptoMasterKey", () => {

View File

@@ -119,8 +119,8 @@ export class AccountKeys {
any,
Record<string, SymmetricCryptoKey>
>();
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
publicKey?: ArrayBuffer;
privateKey?: EncryptionPair<string, Uint8Array> = new EncryptionPair<string, Uint8Array>();
publicKey?: Uint8Array;
apiKeyClientSecret?: string;
toJSON() {
@@ -142,11 +142,10 @@ export class AccountKeys {
),
organizationKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.organizationKeys),
providerKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.providerKeys),
privateKey: EncryptionPair.fromJSON<string, ArrayBuffer>(
obj?.privateKey,
(decObj: string) => Utils.fromByteStringToArray(decObj).buffer
privateKey: EncryptionPair.fromJSON<string, Uint8Array>(obj?.privateKey, (decObj: string) =>
Utils.fromByteStringToArray(decObj)
),
publicKey: Utils.fromByteStringToArray(obj?.publicKey)?.buffer,
publicKey: Utils.fromByteStringToArray(obj?.publicKey),
});
}
@@ -233,6 +232,7 @@ export class AccountSettings {
approveLoginRequests?: boolean;
avatarColor?: string;
activateAutoFillOnPageLoadFromPolicy?: boolean;
region?: string;
smOnboardingTasks?: Record<string, Record<string, boolean>>;
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {

View File

@@ -20,7 +20,7 @@ describe("encArrayBuffer", () => {
array.set(mac, 1 + iv.byteLength);
array.set(data, 1 + iv.byteLength + mac.byteLength);
const actual = new EncArrayBuffer(array.buffer);
const actual = new EncArrayBuffer(array);
expect(actual.encryptionType).toEqual(encType);
expect(actual.ivBytes).toEqualBuffer(iv);
@@ -39,11 +39,11 @@ describe("encArrayBuffer", () => {
array.set(iv, 1);
array.set(data, 1 + iv.byteLength);
const actual = new EncArrayBuffer(array.buffer);
const actual = new EncArrayBuffer(array);
expect(actual.encryptionType).toEqual(encType);
expect(actual.ivBytes).toEqualBuffer(iv);
expect(actual.dataBytes).toEqualBuffer(data);
expect(actual.ivBytes).toEqual(iv);
expect(actual.dataBytes).toEqual(data);
expect(actual.macBytes).toBeNull();
});
});
@@ -58,13 +58,11 @@ describe("encArrayBuffer", () => {
// Minus 1 to leave room for the encType, minus 1 to make it invalid
const invalidBytes = makeStaticByteArray(minLength - 2);
const invalidArray = new Uint8Array(1 + invalidBytes.buffer.byteLength);
const invalidArray = new Uint8Array(1 + invalidBytes.byteLength);
invalidArray.set([encType]);
invalidArray.set(invalidBytes, 1);
expect(() => new EncArrayBuffer(invalidArray.buffer)).toThrow(
"Error parsing encrypted ArrayBuffer"
);
expect(() => new EncArrayBuffer(invalidArray)).toThrow("Error parsing encrypted ArrayBuffer");
});
});

View File

@@ -9,12 +9,12 @@ const MIN_DATA_LENGTH = 1;
export class EncArrayBuffer implements Encrypted {
readonly encryptionType: EncryptionType = null;
readonly dataBytes: ArrayBuffer = null;
readonly ivBytes: ArrayBuffer = null;
readonly macBytes: ArrayBuffer = null;
readonly dataBytes: Uint8Array = null;
readonly ivBytes: Uint8Array = null;
readonly macBytes: Uint8Array = null;
constructor(readonly buffer: ArrayBuffer) {
const encBytes = new Uint8Array(buffer);
constructor(readonly buffer: Uint8Array) {
const encBytes = buffer;
const encType = encBytes[0];
switch (encType) {
@@ -25,12 +25,12 @@ export class EncArrayBuffer implements Encrypted {
this.throwDecryptionError();
}
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH).buffer;
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
this.macBytes = encBytes.slice(
ENC_TYPE_LENGTH + IV_LENGTH,
ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH
).buffer;
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH).buffer;
);
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH);
break;
}
case EncryptionType.AesCbc256_B64: {
@@ -39,8 +39,8 @@ export class EncArrayBuffer implements Encrypted {
this.throwDecryptionError();
}
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH).buffer;
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH).buffer;
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH);
break;
}
default:
@@ -63,11 +63,11 @@ export class EncArrayBuffer implements Encrypted {
if (buffer == null) {
throw new Error("Cannot create EncArrayBuffer from Response - Response is empty");
}
return new EncArrayBuffer(buffer);
return new EncArrayBuffer(new Uint8Array(buffer));
}
static fromB64(b64: string) {
const buffer = Utils.fromB64ToArray(b64).buffer;
const buffer = Utils.fromB64ToArray(b64);
return new EncArrayBuffer(buffer);
}
}

View File

@@ -27,16 +27,16 @@ export class EncString implements Encrypted {
}
}
get ivBytes(): ArrayBuffer {
return this.iv == null ? null : Utils.fromB64ToArray(this.iv).buffer;
get ivBytes(): Uint8Array {
return this.iv == null ? null : Utils.fromB64ToArray(this.iv);
}
get macBytes(): ArrayBuffer {
return this.mac == null ? null : Utils.fromB64ToArray(this.mac).buffer;
get macBytes(): Uint8Array {
return this.mac == null ? null : Utils.fromB64ToArray(this.mac);
}
get dataBytes(): ArrayBuffer {
return this.data == null ? null : Utils.fromB64ToArray(this.data).buffer;
get dataBytes(): Uint8Array {
return this.data == null ? null : Utils.fromB64ToArray(this.data);
}
toJSON() {

View File

@@ -1,8 +1,8 @@
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
export class EncryptedObject {
iv: ArrayBuffer;
data: ArrayBuffer;
mac: ArrayBuffer;
iv: Uint8Array;
data: Uint8Array;
mac: Uint8Array;
key: SymmetricCryptoKey;
}

View File

@@ -11,6 +11,13 @@ describe("EncryptionPair", () => {
expect(json.decrypted).toEqual("hello");
});
it("should populate decryptedSerialized for TypesArrays", () => {
const pair = new EncryptionPair<string, Uint8Array>();
pair.decrypted = Utils.fromByteStringToArray("hello");
const json = pair.toJSON();
expect(json.decrypted).toEqual(new Uint8Array([104, 101, 108, 108, 111]));
});
it("should serialize encrypted and decrypted", () => {
const pair = new EncryptionPair<string, string>();
pair.encrypted = "hello";

View File

@@ -36,4 +36,5 @@ export class GlobalState {
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
region?: string;
}

View File

@@ -68,7 +68,7 @@ describe("SymmetricCryptoKey", () => {
});
it("toJSON creates object for serialization", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64).buffer);
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const actual = key.toJSON();
const expected = { keyB64: key.keyB64 };
@@ -77,7 +77,7 @@ describe("SymmetricCryptoKey", () => {
});
it("fromJSON hydrates new object", () => {
const expected = new SymmetricCryptoKey(makeStaticByteArray(64).buffer);
const expected = new SymmetricCryptoKey(makeStaticByteArray(64));
const actual = SymmetricCryptoKey.fromJSON({ keyB64: expected.keyB64 });
expect(actual).toEqual(expected);

View File

@@ -4,9 +4,9 @@ import { EncryptionType } from "../../../enums";
import { Utils } from "../../../platform/misc/utils";
export class SymmetricCryptoKey {
key: ArrayBuffer;
encKey?: ArrayBuffer;
macKey?: ArrayBuffer;
key: Uint8Array;
encKey?: Uint8Array;
macKey?: Uint8Array;
encType: EncryptionType;
keyB64: string;
@@ -15,7 +15,7 @@ export class SymmetricCryptoKey {
meta: any;
constructor(key: ArrayBuffer, encType?: EncryptionType) {
constructor(key: Uint8Array, encType?: EncryptionType) {
if (key == null) {
throw new Error("Must provide key");
}
@@ -67,7 +67,7 @@ export class SymmetricCryptoKey {
return null;
}
const arrayBuffer = Utils.fromB64ToArray(s).buffer;
const arrayBuffer = Utils.fromB64ToArray(s);
return new SymmetricCryptoKey(arrayBuffer);
}

View File

@@ -1,4 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
import { Region } from "../../abstractions/environment.service";
export class ServerConfigResponse extends BaseResponse {
version: string;
@@ -23,6 +24,7 @@ export class ServerConfigResponse extends BaseResponse {
}
export class EnvironmentServerConfigResponse extends BaseResponse {
cloudRegion: Region;
vault: string;
api: string;
identity: string;
@@ -36,6 +38,7 @@ export class EnvironmentServerConfigResponse extends BaseResponse {
return;
}
this.cloudRegion = this.getResponseProperty("CloudRegion");
this.vault = this.getResponseProperty("Vault");
this.api = this.getResponseProperty("Api");
this.identity = this.getResponseProperty("Identity");

View File

@@ -44,6 +44,7 @@ export class ConfigService implements ConfigServiceAbstraction {
return serverConfig;
}
await this.stateService.setServerConfig(data);
this.environmentService.setCloudWebVaultUrl(data.environment?.cloudRegion);
}
} catch {
return null;
@@ -62,6 +63,11 @@ export class ConfigService implements ConfigServiceAbstraction {
return await this.getFeatureFlag(key, defaultValue);
}
async getCloudRegion(defaultValue = "US"): Promise<string> {
const serverConfig = await this.buildServerConfig();
return serverConfig.environment?.cloudRegion ?? defaultValue;
}
private async getFeatureFlag<T>(key: FeatureFlag, defaultValue: T): Promise<T> {
const serverConfig = await this.buildServerConfig();
if (

View File

@@ -123,7 +123,7 @@ export class CryptoService implements CryptoServiceAbstraction {
): Promise<SymmetricCryptoKey> {
const key = await this.retrieveKeyFromStorage(keySuffix, userId);
if (key != null) {
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key));
if (!(await this.validateKey(symmetricKey))) {
this.logService.warning("Wrong key, throwing away stored key");
@@ -172,7 +172,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.getEncKeyHelper(key);
}
async getPublicKey(): Promise<ArrayBuffer> {
async getPublicKey(): Promise<Uint8Array> {
const inMemoryPublicKey = await this.stateService.getPublicKey();
if (inMemoryPublicKey != null) {
return inMemoryPublicKey;
@@ -188,7 +188,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return publicKey;
}
async getPrivateKey(): Promise<ArrayBuffer> {
async getPrivateKey(): Promise<Uint8Array> {
const decryptedPrivateKey = await this.stateService.getDecryptedPrivateKey();
if (decryptedPrivateKey != null) {
return decryptedPrivateKey;
@@ -204,7 +204,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return privateKey;
}
async getFingerprint(userId: string, publicKey?: ArrayBuffer): Promise<string[]> {
async getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise<string[]> {
if (publicKey == null) {
publicKey = await this.getPublicKey();
}
@@ -214,7 +214,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const keyFingerprint = await this.cryptoFunctionService.hash(publicKey, "sha256");
const userFingerprint = await this.cryptoFunctionService.hkdfExpand(
keyFingerprint,
userId,
fingerprintMaterial,
32,
"sha256"
);
@@ -416,7 +416,7 @@ export class CryptoService implements CryptoServiceAbstraction {
kdf: KdfType,
kdfConfig: KdfConfig
): Promise<SymmetricCryptoKey> {
let key: ArrayBuffer = null;
let key: Uint8Array = null;
if (kdf == null || kdf === KdfType.PBKDF2_SHA256) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = 5000;
@@ -502,7 +502,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return await this.stretchKey(pinKey);
}
async makeSendKey(keyMaterial: ArrayBuffer): Promise<SymmetricCryptoKey> {
async makeSendKey(keyMaterial: Uint8Array): Promise<SymmetricCryptoKey> {
const sendKey = await this.cryptoFunctionService.hkdf(
keyMaterial,
"bitwarden-send",
@@ -550,7 +550,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encrypt
*/
async encrypt(plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncString> {
async encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey): Promise<EncString> {
key = await this.getKeyForUserEncryption(key);
return await this.encryptService.encrypt(plainValue, key);
}
@@ -559,12 +559,12 @@ export class CryptoService implements CryptoServiceAbstraction {
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.encryptToBytes
*/
async encryptToBytes(plainValue: ArrayBuffer, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
async encryptToBytes(plainValue: Uint8Array, key?: SymmetricCryptoKey): Promise<EncArrayBuffer> {
key = await this.getKeyForUserEncryption(key);
return this.encryptService.encryptToBytes(plainValue, key);
}
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
async rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString> {
if (publicKey == null) {
publicKey = await this.getPublicKey();
}
@@ -576,7 +576,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
}
async rsaDecrypt(encValue: string, privateKeyValue?: ArrayBuffer): Promise<ArrayBuffer> {
async rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array> {
const headerPieces = encValue.split(".");
let encType: EncryptionType = null;
let encPieces: string[];
@@ -607,7 +607,7 @@ export class CryptoService implements CryptoServiceAbstraction {
throw new Error("encPieces unavailable.");
}
const data = Utils.fromB64ToArray(encPieces[0]).buffer;
const data = Utils.fromB64ToArray(encPieces[0]);
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
if (privateKey == null) {
throw new Error("No private key.");
@@ -633,7 +633,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToBytes
*/
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<Uint8Array> {
const keyForEnc = await this.getKeyForUserEncryption(key);
return this.encryptService.decryptToBytes(encString, keyForEnc);
}
@@ -651,7 +651,7 @@ export class CryptoService implements CryptoServiceAbstraction {
* @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey)
* and then call encryptService.decryptToBytes
*/
async decryptFromBytes(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
async decryptFromBytes(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
if (encBuffer == null) {
throw new Error("No buffer provided for decryption.");
}
@@ -768,10 +768,10 @@ export class CryptoService implements CryptoServiceAbstraction {
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey.buffer);
return new SymmetricCryptoKey(newKey);
}
private async hashPhrase(hash: ArrayBuffer, minimumEntropy = 64) {
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
const entropyPerWord = Math.log(EFFLongWordList.length) / Math.log(2);
let numWords = Math.ceil(minimumEntropy / entropyPerWord);
@@ -793,7 +793,7 @@ export class CryptoService implements CryptoServiceAbstraction {
private async buildEncKey(
key: SymmetricCryptoKey,
encKey: ArrayBuffer
encKey: Uint8Array
): Promise<[SymmetricCryptoKey, EncString]> {
let encKeyEnc: EncString = null;
if (key.key.byteLength === 32) {
@@ -830,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
return null;
}
let decEncKey: ArrayBuffer;
let decEncKey: Uint8Array;
const encKeyCipher = new EncString(encKey);
if (encKeyCipher.encryptionType === EncryptionType.AesCbc256_B64) {
decEncKey = await this.decryptToBytes(encKeyCipher, key);

View File

@@ -18,7 +18,7 @@ export class EncryptServiceImplementation implements EncryptService {
protected logMacFailures: boolean
) {}
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
@@ -27,9 +27,9 @@ export class EncryptServiceImplementation implements EncryptService {
return Promise.resolve(null);
}
let plainBuf: ArrayBuffer;
let plainBuf: Uint8Array;
if (typeof plainValue === "string") {
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
plainBuf = Utils.fromUtf8ToArray(plainValue);
} else {
plainBuf = plainValue;
}
@@ -41,7 +41,7 @@ export class EncryptServiceImplementation implements EncryptService {
return new EncString(encObj.key.encType, data, iv, mac);
}
async encryptToBytes(plainValue: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
if (key == null) {
throw new Error("No encryption key provided.");
}
@@ -60,7 +60,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
return new EncArrayBuffer(encBytes.buffer);
return new EncArrayBuffer(encBytes);
}
async decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
@@ -102,7 +102,7 @@ export class EncryptServiceImplementation implements EncryptService {
return await this.cryptoFunctionService.aesDecryptFast(fastParams);
}
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<ArrayBuffer> {
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
if (key == null) {
throw new Error("No encryption key provided.");
}
@@ -125,11 +125,7 @@ export class EncryptServiceImplementation implements EncryptService {
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
macData.set(new Uint8Array(encThing.ivBytes), 0);
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(
macData.buffer,
key.macKey,
"sha256"
);
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
if (computedMac === null) {
return null;
}
@@ -161,7 +157,7 @@ export class EncryptServiceImplementation implements EncryptService {
return await Promise.all(items.map((item) => item.decrypt(key)));
}
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.key = key;
obj.iv = await this.cryptoFunctionService.randomBytes(16);
@@ -171,7 +167,7 @@ export class EncryptServiceImplementation implements EncryptService {
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
macData.set(new Uint8Array(obj.iv), 0);
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
obj.mac = await this.cryptoFunctionService.hmac(macData, obj.key.macKey, "sha256");
}
return obj;

View File

@@ -37,10 +37,8 @@ describe("EncryptService", () => {
describe("encrypts data", () => {
beforeEach(() => {
cryptoFunctionService.randomBytes
.calledWith(16)
.mockResolvedValueOnce(iv.buffer as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData.buffer);
cryptoFunctionService.randomBytes.calledWith(16).mockResolvedValueOnce(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData);
});
it("using a key which supports mac", async () => {
@@ -50,7 +48,7 @@ describe("EncryptService", () => {
key.macKey = makeStaticByteArray(16, 20);
cryptoFunctionService.hmac.mockResolvedValue(mac.buffer);
cryptoFunctionService.hmac.mockResolvedValue(mac);
const actual = await encryptService.encryptToBytes(plainValue, key);
@@ -86,7 +84,7 @@ describe("EncryptService", () => {
describe("decryptToBytes", () => {
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType);
const computedMac = new Uint8Array(1).buffer;
const computedMac = new Uint8Array(1);
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, encType));
beforeEach(() => {
@@ -106,9 +104,9 @@ describe("EncryptService", () => {
});
it("decrypts data with provided key", async () => {
const decryptedBytes = makeStaticByteArray(10, 200).buffer;
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1).buffer);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
cryptoFunctionService.compare.mockResolvedValue(true);
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);

View File

@@ -3,6 +3,7 @@ import { concatMap, Observable, Subject } from "rxjs";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import {
EnvironmentService as EnvironmentServiceAbstraction,
Region,
Urls,
} from "../abstractions/environment.service";
import { StateService } from "../abstractions/state.service";
@@ -10,6 +11,8 @@ import { StateService } from "../abstractions/state.service";
export class EnvironmentService implements EnvironmentServiceAbstraction {
private readonly urlsSubject = new Subject<void>();
urls: Observable<void> = this.urlsSubject.asObservable();
selectedRegion?: Region;
initialized = false;
protected baseUrl: string;
protected webVaultUrl: string;
@@ -20,11 +23,37 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
protected eventsUrl: string;
private keyConnectorUrl: string;
private scimUrl: string = null;
private cloudWebVaultUrl: string;
readonly usUrls: Urls = {
base: null,
api: "https://api.bitwarden.com",
identity: "https://identity.bitwarden.com",
icons: "https://icons.bitwarden.net",
webVault: "https://vault.bitwarden.com",
notifications: "https://notifications.bitwarden.com",
events: "https://events.bitwarden.com",
scim: "https://scim.bitwarden.com",
};
readonly euUrls: Urls = {
base: null,
api: "https://api.bitwarden.eu",
identity: "https://identity.bitwarden.eu",
icons: "https://icons.bitwarden.eu",
webVault: "https://vault.bitwarden.eu",
notifications: "https://notifications.bitwarden.eu",
events: "https://events.bitwarden.eu",
scim: "https://scim.bitwarden.eu",
};
constructor(private stateService: StateService) {
this.stateService.activeAccount$
.pipe(
concatMap(async () => {
if (!this.initialized) {
return;
}
await this.setUrlsFromStorage();
})
)
@@ -58,6 +87,26 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return "https://vault.bitwarden.com";
}
getCloudWebVaultUrl() {
if (this.cloudWebVaultUrl != null) {
return this.cloudWebVaultUrl;
}
return this.usUrls.webVault;
}
setCloudWebVaultUrl(region: Region) {
switch (region) {
case Region.EU:
this.cloudWebVaultUrl = this.euUrls.webVault;
break;
case Region.US:
default:
this.cloudWebVaultUrl = this.usUrls.webVault;
break;
}
}
getSendUrl() {
return this.getWebVaultUrl() === "https://vault.bitwarden.com"
? "https://send.bitwarden.com/#"
@@ -127,20 +176,42 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}
async setUrlsFromStorage(): Promise<void> {
const urls: any = await this.stateService.getEnvironmentUrls();
const region = await this.stateService.getRegion();
const savedUrls = await this.stateService.getEnvironmentUrls();
const envUrls = new EnvironmentUrls();
this.baseUrl = envUrls.base = urls.base;
this.webVaultUrl = urls.webVault;
this.apiUrl = envUrls.api = urls.api;
this.identityUrl = envUrls.identity = urls.identity;
this.iconsUrl = urls.icons;
this.notificationsUrl = urls.notifications;
this.eventsUrl = envUrls.events = urls.events;
this.keyConnectorUrl = urls.keyConnector;
// scimUrl is not saved to storage
// In release `2023.5.0`, we set the `base` property of the environment URLs to the US web vault URL when a user clicked the "US" region.
// This check will detect these cases and convert them to the proper region instead.
// We are detecting this by checking for the presence of the web vault URL in the `base` and the absence of the `notifications` property.
// This is because the `notifications` will not be `null` in the web vault, and we don't want to migrate the URLs in that case.
if (savedUrls.base === "https://vault.bitwarden.com" && savedUrls.notifications == null) {
await this.setRegion(Region.US);
return;
}
this.urlsSubject.next();
switch (region) {
case Region.EU:
await this.setRegion(Region.EU);
return;
case Region.US:
await this.setRegion(Region.US);
return;
case Region.SelfHosted:
case null:
default:
this.baseUrl = envUrls.base = savedUrls.base;
this.webVaultUrl = savedUrls.webVault;
this.apiUrl = envUrls.api = savedUrls.api;
this.identityUrl = envUrls.identity = savedUrls.identity;
this.iconsUrl = savedUrls.icons;
this.notificationsUrl = savedUrls.notifications;
this.eventsUrl = envUrls.events = savedUrls.events;
this.keyConnectorUrl = savedUrls.keyConnector;
await this.setRegion(Region.SelfHosted);
// scimUrl is not saved to storage
this.urlsSubject.next();
break;
}
}
async setUrls(urls: Urls): Promise<Urls> {
@@ -178,6 +249,8 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
this.keyConnectorUrl = urls.keyConnector;
this.scimUrl = urls.scim;
await this.setRegion(Region.SelfHosted);
this.urlsSubject.next();
return urls;
@@ -187,6 +260,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
return {
base: this.baseUrl,
webVault: this.webVaultUrl,
cloudWebVault: this.cloudWebVaultUrl,
api: this.apiUrl,
identity: this.identityUrl,
icons: this.iconsUrl,
@@ -197,6 +271,53 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
};
}
isEmpty(): boolean {
return (
this.baseUrl == null &&
this.webVaultUrl == null &&
this.apiUrl == null &&
this.identityUrl == null &&
this.iconsUrl == null &&
this.notificationsUrl == null &&
this.eventsUrl == null
);
}
async setRegion(region: Region) {
this.selectedRegion = region;
await this.stateService.setRegion(region);
if (region === Region.SelfHosted) {
// If user saves a self-hosted region with empty fields, default to US
if (this.isEmpty()) {
await this.setRegion(Region.US);
}
} else {
// If we are setting the region to EU or US, clear the self-hosted URLs
await this.stateService.setEnvironmentUrls(new EnvironmentUrls());
if (region === Region.EU) {
this.setUrlsInternal(this.euUrls);
} else if (region === Region.US) {
this.setUrlsInternal(this.usUrls);
}
}
}
private setUrlsInternal(urls: Urls) {
this.baseUrl = this.formatUrl(urls.base);
this.webVaultUrl = this.formatUrl(urls.webVault);
this.apiUrl = this.formatUrl(urls.api);
this.identityUrl = this.formatUrl(urls.identity);
this.iconsUrl = this.formatUrl(urls.icons);
this.notificationsUrl = this.formatUrl(urls.notifications);
this.eventsUrl = this.formatUrl(urls.events);
this.keyConnectorUrl = this.formatUrl(urls.keyConnector);
// scimUrl cannot be cleared
this.scimUrl = this.formatUrl(urls.scim) ?? this.scimUrl;
this.urlsSubject.next();
}
private formatUrl(url: string): string {
if (url == null || url === "") {
return null;
@@ -211,19 +332,11 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
}
isCloud(): boolean {
return ["https://api.bitwarden.com", "https://vault.bitwarden.com/api"].includes(
this.getApiUrl()
);
}
isSelfHosted(): boolean {
return ![
"http://vault.bitwarden.com",
"https://vault.bitwarden.com",
"http://vault.bitwarden.eu",
"https://vault.bitwarden.eu",
"http://vault.qa.bitwarden.pw",
"https://vault.qa.bitwarden.pw",
].includes(this.getWebVaultUrl());
return [
"https://api.bitwarden.com",
"https://vault.bitwarden.com/api",
"https://api.bitwarden.eu",
"https://vault.bitwarden.eu/api",
].includes(this.getApiUrl());
}
}

View File

@@ -179,7 +179,7 @@ export class StateService<
}
async addAccount(account: TAccount) {
account = await this.setAccountEnvironmentUrls(account);
account = await this.setAccountEnvironment(account);
await this.updateState(async (state) => {
state.authenticatedAccounts.push(account.profile.userId);
await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts);
@@ -763,13 +763,13 @@ export class StateService<
);
}
async getDecryptedPrivateKey(options?: StorageOptions): Promise<ArrayBuffer> {
async getDecryptedPrivateKey(options?: StorageOptions): Promise<Uint8Array> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys?.privateKey.decrypted;
}
async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
async setDecryptedPrivateKey(value: Uint8Array, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
@@ -1598,6 +1598,28 @@ export class StateService<
);
}
async getRegion(options?: StorageOptions): Promise<string> {
if ((await this.state())?.activeUserId == null) {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).region ?? null;
}
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getAccount(options))?.settings?.region ?? null;
}
async setRegion(value: string, options?: StorageOptions): Promise<void> {
// Global values are set on each change and the current global settings are passed to any newly authed accounts.
// This is to allow setting region values before an account is active, while still allowing individual accounts to have their own region.
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.region = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
@@ -2075,14 +2097,14 @@ export class StateService<
);
}
async getPublicKey(options?: StorageOptions): Promise<ArrayBuffer> {
async getPublicKey(options?: StorageOptions): Promise<Uint8Array> {
const keys = (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.keys;
return keys?.publicKey;
}
async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise<void> {
async setPublicKey(value: Uint8Array, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
);
@@ -2584,8 +2606,9 @@ export class StateService<
await this.defaultOnDiskLocalOptions()
)
);
// EnvironmentUrls are set before authenticating and should override whatever is stored from any previous session
// EnvironmentUrls and region are set before authenticating and should override whatever is stored from any previous session
const environmentUrls = account.settings.environmentUrls;
const region = account.settings.region;
if (storedAccount?.settings != null) {
account.settings = storedAccount.settings;
} else if (await this.storageService.has(keys.tempAccountSettings)) {
@@ -2593,6 +2616,8 @@ export class StateService<
await this.storageService.remove(keys.tempAccountSettings);
}
account.settings.environmentUrls = environmentUrls;
account.settings.region = region;
if (
account.settings.vaultTimeoutAction === VaultTimeoutAction.LogOut &&
account.settings.vaultTimeout != null
@@ -2620,6 +2645,7 @@ export class StateService<
);
if (storedAccount?.settings != null) {
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
storedAccount.settings.region = account.settings.region;
account.settings = storedAccount.settings;
}
await this.storageService.save(
@@ -2642,6 +2668,7 @@ export class StateService<
);
if (storedAccount?.settings != null) {
storedAccount.settings.environmentUrls = account.settings.environmentUrls;
storedAccount.settings.region = account.settings.region;
account.settings = storedAccount.settings;
}
await this.storageService.save(
@@ -2790,7 +2817,9 @@ export class StateService<
return Object.assign(this.createAccount(), persistentAccountInformation);
}
protected async setAccountEnvironmentUrls(account: TAccount): Promise<TAccount> {
// The environment urls and region are selected before login and are transferred here to an authenticated account
protected async setAccountEnvironment(account: TAccount): Promise<TAccount> {
account.settings.region = await this.getGlobalRegion();
account.settings.environmentUrls = await this.getGlobalEnvironmentUrls();
return account;
}
@@ -2800,6 +2829,11 @@ export class StateService<
return (await this.getGlobals(options)).environmentUrls ?? new EnvironmentUrls();
}
protected async getGlobalRegion(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultOnDiskOptions());
return (await this.getGlobals(options)).region ?? null;
}
protected async clearDecryptedDataForActiveUser(): Promise<void> {
await this.updateState(async (state) => {
const userId = state?.activeUserId;

View File

@@ -160,7 +160,7 @@ describe("WebCrypto Function Service", () => {
const a = new Uint8Array(2);
a[0] = 1;
a[1] = 2;
const equal = await cryptoFunctionService.compare(a.buffer, a.buffer);
const equal = await cryptoFunctionService.compare(a, a);
expect(equal).toBe(true);
});
@@ -172,7 +172,7 @@ describe("WebCrypto Function Service", () => {
const b = new Uint8Array(2);
b[0] = 3;
b[1] = 4;
const equal = await cryptoFunctionService.compare(a.buffer, b.buffer);
const equal = await cryptoFunctionService.compare(a, b);
expect(equal).toBe(false);
});
@@ -183,7 +183,7 @@ describe("WebCrypto Function Service", () => {
a[1] = 2;
const b = new Uint8Array(2);
b[0] = 3;
const equal = await cryptoFunctionService.compare(a.buffer, b.buffer);
const equal = await cryptoFunctionService.compare(a, b);
expect(equal).toBe(false);
});
});
@@ -200,7 +200,7 @@ describe("WebCrypto Function Service", () => {
const a = new Uint8Array(2);
a[0] = 1;
a[1] = 2;
const aByteString = Utils.fromBufferToByteString(a.buffer);
const aByteString = Utils.fromBufferToByteString(a);
const equal = await cryptoFunctionService.compareFast(aByteString, aByteString);
expect(equal).toBe(true);
});
@@ -210,11 +210,11 @@ describe("WebCrypto Function Service", () => {
const a = new Uint8Array(2);
a[0] = 1;
a[1] = 2;
const aByteString = Utils.fromBufferToByteString(a.buffer);
const aByteString = Utils.fromBufferToByteString(a);
const b = new Uint8Array(2);
b[0] = 3;
b[1] = 4;
const bByteString = Utils.fromBufferToByteString(b.buffer);
const bByteString = Utils.fromBufferToByteString(b);
const equal = await cryptoFunctionService.compareFast(aByteString, bByteString);
expect(equal).toBe(false);
});
@@ -224,10 +224,10 @@ describe("WebCrypto Function Service", () => {
const a = new Uint8Array(2);
a[0] = 1;
a[1] = 2;
const aByteString = Utils.fromBufferToByteString(a.buffer);
const aByteString = Utils.fromBufferToByteString(a);
const b = new Uint8Array(2);
b[0] = 3;
const bByteString = Utils.fromBufferToByteString(b.buffer);
const bByteString = Utils.fromBufferToByteString(b);
const equal = await cryptoFunctionService.compareFast(aByteString, bByteString);
expect(equal).toBe(false);
});
@@ -239,7 +239,7 @@ describe("WebCrypto Function Service", () => {
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const data = Utils.fromUtf8ToArray("EncryptMe!");
const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA==");
});
@@ -249,10 +249,10 @@ describe("WebCrypto Function Service", () => {
const key = makeStaticByteArray(32);
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
const encData = Utils.fromBufferToB64(encValue);
const b64Iv = Utils.fromBufferToB64(iv.buffer);
const symKey = new SymmetricCryptoKey(key.buffer);
const b64Iv = Utils.fromBufferToB64(iv);
const symKey = new SymmetricCryptoKey(key);
const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast(params);
expect(decValue).toBe(value);
@@ -264,8 +264,8 @@ describe("WebCrypto Function Service", () => {
const key = makeStaticByteArray(32);
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = await cryptoFunctionService.aesEncrypt(data.buffer, iv.buffer, key.buffer);
const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer);
const encValue = new Uint8Array(await cryptoFunctionService.aesEncrypt(data, iv, key));
const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv, key);
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
});
});
@@ -273,8 +273,8 @@ describe("WebCrypto Function Service", () => {
describe("aesDecryptFast", () => {
it("should successfully decrypt data", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer);
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer);
const iv = Utils.fromBufferToB64(makeStaticByteArray(16));
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32));
const data = "ByUF8vhyX4ddU9gcooznwA==";
const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast(params);
@@ -288,7 +288,7 @@ describe("WebCrypto Function Service", () => {
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
const decValue = await cryptoFunctionService.aesDecrypt(data.buffer, iv.buffer, key.buffer);
const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key);
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
});
});
@@ -300,8 +300,8 @@ describe("WebCrypto Function Service", () => {
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = await cryptoFunctionService.rsaEncrypt(data.buffer, pubKey.buffer, "sha1");
const decValue = await cryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, "sha1");
const encValue = new Uint8Array(await cryptoFunctionService.rsaEncrypt(data, pubKey, "sha1"));
const decValue = await cryptoFunctionService.rsaDecrypt(encValue, privKey, "sha1");
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
});
});
@@ -316,7 +316,7 @@ describe("WebCrypto Function Service", () => {
"zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D" +
"/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="
);
const decValue = await cryptoFunctionService.rsaDecrypt(data.buffer, privKey.buffer, "sha1");
const decValue = await cryptoFunctionService.rsaDecrypt(data, privKey, "sha1");
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
});
});
@@ -325,7 +325,7 @@ describe("WebCrypto Function Service", () => {
it("should successfully extract key", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(privKey.buffer);
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(privKey);
expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey);
});
});
@@ -390,8 +390,8 @@ function testPbkdf2(
it("should create valid " + algorithm + " key from array buffer input", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const key = await cryptoFunctionService.pbkdf2(
Utils.fromUtf8ToArray(regularPassword).buffer,
Utils.fromUtf8ToArray(regularEmail).buffer,
Utils.fromUtf8ToArray(regularPassword),
Utils.fromUtf8ToArray(regularEmail),
algorithm,
5000
);
@@ -437,8 +437,8 @@ function testHkdf(
const cryptoFunctionService = getWebCryptoFunctionService();
const key = await cryptoFunctionService.hkdf(
ikm,
Utils.fromUtf8ToArray(regularSalt).buffer,
Utils.fromUtf8ToArray(regularInfo).buffer,
Utils.fromUtf8ToArray(regularSalt),
Utils.fromUtf8ToArray(regularInfo),
32,
algorithm
);
@@ -496,10 +496,7 @@ function testHash(
it("should create valid " + algorithm + " hash from array buffer input", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const hash = await cryptoFunctionService.hash(
Utils.fromUtf8ToArray(regularValue).buffer,
algorithm
);
const hash = await cryptoFunctionService.hash(Utils.fromUtf8ToArray(regularValue), algorithm);
expect(Utils.fromBufferToHex(hash)).toBe(regularHash);
});
}
@@ -508,8 +505,8 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
it("should create valid " + algorithm + " hmac", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const computedMac = await cryptoFunctionService.hmac(
Utils.fromUtf8ToArray("SignMe!!").buffer,
Utils.fromUtf8ToArray("secretkey").buffer,
Utils.fromUtf8ToArray("SignMe!!"),
Utils.fromUtf8ToArray("secretkey"),
algorithm
);
expect(Utils.fromBufferToHex(computedMac)).toBe(mac);
@@ -519,14 +516,14 @@ function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
function testHmacFast(algorithm: "sha1" | "sha256" | "sha512", mac: string) {
it("should create valid " + algorithm + " hmac", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("secretkey").buffer);
const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("SignMe!!").buffer);
const keyByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("secretkey"));
const dataByteString = Utils.fromBufferToByteString(Utils.fromUtf8ToArray("SignMe!!"));
const computedMac = await cryptoFunctionService.hmacFast(
dataByteString,
keyByteString,
algorithm
);
expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac).buffer)).toBe(mac);
expect(Utils.fromBufferToHex(Utils.fromByteStringToArray(computedMac))).toBe(mac);
});
}
@@ -535,7 +532,9 @@ function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) {
"should successfully generate a " + length + " bit key pair",
async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length);
const keyPair = (await cryptoFunctionService.rsaGenerateKeyPair(length)).map(
(k) => new Uint8Array(k)
);
expect(keyPair[0] == null || keyPair[1] == null).toBe(false);
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]);
expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey));

View File

@@ -20,11 +20,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
}
async pbkdf2(
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
password: string | Uint8Array,
salt: string | Uint8Array,
algorithm: "sha256" | "sha512",
iterations: number
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
const wcLen = algorithm === "sha256" ? 256 : 512;
const passwordBuf = this.toBuf(password);
const saltBuf = this.toBuf(salt);
@@ -43,16 +43,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
false,
["deriveBits"]
);
return await this.subtle.deriveBits(pbkdf2Params, impKey, wcLen);
const buffer = await this.subtle.deriveBits(pbkdf2Params as any, impKey, wcLen);
return new Uint8Array(buffer);
}
async argon2(
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
password: string | Uint8Array,
salt: string | Uint8Array,
iterations: number,
memory: number,
parallelism: number
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
if (!this.wasmSupported) {
throw "Webassembly support is required for the Argon2 KDF feature.";
}
@@ -69,16 +70,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
hashLen: 32,
type: argon2.ArgonType.Argon2id,
});
argon2.unloadRuntime();
return result.hash;
}
async hkdf(
ikm: ArrayBuffer,
salt: string | ArrayBuffer,
info: string | ArrayBuffer,
ikm: Uint8Array,
salt: string | Uint8Array,
info: string | Uint8Array,
outputByteSize: number,
algorithm: "sha256" | "sha512"
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
const saltBuf = this.toBuf(salt);
const infoBuf = this.toBuf(info);
@@ -92,16 +94,17 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
const impKey = await this.subtle.importKey("raw", ikm, { name: "HKDF" } as any, false, [
"deriveBits",
]);
return await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
const buffer = await this.subtle.deriveBits(hkdfParams as any, impKey, outputByteSize * 8);
return new Uint8Array(buffer);
}
// ref: https://tools.ietf.org/html/rfc5869
async hkdfExpand(
prk: ArrayBuffer,
info: string | ArrayBuffer,
prk: Uint8Array,
info: string | Uint8Array,
outputByteSize: number,
algorithm: "sha256" | "sha512"
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
const hashLen = algorithm === "sha256" ? 32 : 64;
if (outputByteSize > 255 * hashLen) {
throw new Error("outputByteSize is too large.");
@@ -121,49 +124,54 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
t.set(previousT);
t.set(infoArr, previousT.length);
t.set([i + 1], t.length - 1);
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
previousT = new Uint8Array(await this.hmac(t, prk, algorithm));
okm.set(previousT, runningOkmLength);
runningOkmLength += previousT.length;
if (runningOkmLength >= outputByteSize) {
break;
}
}
return okm.slice(0, outputByteSize).buffer;
return okm.slice(0, outputByteSize);
}
async hash(
value: string | ArrayBuffer,
value: string | Uint8Array,
algorithm: "sha1" | "sha256" | "sha512" | "md5"
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
if (algorithm === "md5") {
const md = algorithm === "md5" ? forge.md.md5.create() : forge.md.sha1.create();
const valueBytes = this.toByteString(value);
md.update(valueBytes, "raw");
return Utils.fromByteStringToArray(md.digest().data).buffer;
return Utils.fromByteStringToArray(md.digest().data);
}
const valueBuf = this.toBuf(value);
return await this.subtle.digest({ name: this.toWebCryptoAlgorithm(algorithm) }, valueBuf);
const buffer = await this.subtle.digest(
{ name: this.toWebCryptoAlgorithm(algorithm) },
valueBuf
);
return new Uint8Array(buffer);
}
async hmac(
value: ArrayBuffer,
key: ArrayBuffer,
value: Uint8Array,
key: Uint8Array,
algorithm: "sha1" | "sha256" | "sha512"
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
const signingAlgorithm = {
name: "HMAC",
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("raw", key, signingAlgorithm, false, ["sign"]);
return await this.subtle.sign(signingAlgorithm, impKey, value);
const buffer = await this.subtle.sign(signingAlgorithm, impKey, value);
return new Uint8Array(buffer);
}
// Safely compare two values in a way that protects against timing attacks (Double HMAC Verification).
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
async compare(a: Uint8Array, b: Uint8Array): Promise<boolean> {
const macKey = await this.randomBytes(32);
const signingAlgorithm = {
name: "HMAC",
@@ -218,11 +226,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return equals;
}
async aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
async aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"encrypt",
]);
return await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
const buffer = await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
return new Uint8Array(buffer);
}
aesDecryptFastParameters(
@@ -274,18 +283,19 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return Promise.resolve(val);
}
async aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
async aesDecrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"decrypt",
]);
return await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
return new Uint8Array(buffer);
}
async rsaEncrypt(
data: ArrayBuffer,
publicKey: ArrayBuffer,
data: Uint8Array,
publicKey: Uint8Array,
algorithm: "sha1" | "sha256"
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
// We cannot use the proper types here.
const rsaParams = {
@@ -293,14 +303,15 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("spki", publicKey, rsaParams, false, ["encrypt"]);
return await this.subtle.encrypt(rsaParams, impKey, data);
const buffer = await this.subtle.encrypt(rsaParams, impKey, data);
return new Uint8Array(buffer);
}
async rsaDecrypt(
data: ArrayBuffer,
privateKey: ArrayBuffer,
data: Uint8Array,
privateKey: Uint8Array,
algorithm: "sha1" | "sha256"
): Promise<ArrayBuffer> {
): Promise<Uint8Array> {
// Note: Edge browser requires that we specify name and hash for both key import and decrypt.
// We cannot use the proper types here.
const rsaParams = {
@@ -308,10 +319,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
hash: { name: this.toWebCryptoAlgorithm(algorithm) },
};
const impKey = await this.subtle.importKey("pkcs8", privateKey, rsaParams, false, ["decrypt"]);
return await this.subtle.decrypt(rsaParams, impKey, data);
const buffer = await this.subtle.decrypt(rsaParams, impKey, data);
return new Uint8Array(buffer);
}
async rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
async rsaExtractPublicKey(privateKey: Uint8Array): Promise<Uint8Array> {
const rsaParams = {
name: "RSA-OAEP",
// Have to specify some algorithm
@@ -331,10 +343,11 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
const impPublicKey = await this.subtle.importKey("jwk", jwkPublicKeyParams, rsaParams, true, [
"encrypt",
]);
return await this.subtle.exportKey("spki", impPublicKey);
const buffer = await this.subtle.exportKey("spki", impPublicKey);
return new Uint8Array(buffer);
}
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]> {
const rsaParams = {
name: "RSA-OAEP",
modulusLength: length,
@@ -348,26 +361,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
])) as CryptoKeyPair;
const publicKey = await this.subtle.exportKey("spki", keyPair.publicKey);
const privateKey = await this.subtle.exportKey("pkcs8", keyPair.privateKey);
return [publicKey, privateKey];
return [new Uint8Array(publicKey), new Uint8Array(privateKey)];
}
randomBytes(length: number): Promise<CsprngArray> {
const arr = new Uint8Array(length);
this.crypto.getRandomValues(arr);
return Promise.resolve(arr.buffer as CsprngArray);
return Promise.resolve(arr as CsprngArray);
}
private toBuf(value: string | ArrayBuffer): ArrayBuffer {
let buf: ArrayBuffer;
private toBuf(value: string | Uint8Array): Uint8Array {
let buf: Uint8Array;
if (typeof value === "string") {
buf = Utils.fromUtf8ToArray(value).buffer;
buf = Utils.fromUtf8ToArray(value);
} else {
buf = value;
}
return buf;
}
private toByteString(value: string | ArrayBuffer): string {
private toByteString(value: string | Uint8Array): string {
let bytes: string;
if (typeof value === "string") {
bytes = forge.util.encodeUtf8(value);

View File

@@ -133,6 +133,7 @@ import { Utils } from "../platform/misc/utils";
import { AttachmentRequest } from "../vault/models/request/attachment.request";
import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request";
import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request";
import { CipherBulkRestoreRequest } from "../vault/models/request/cipher-bulk-restore.request";
import { CipherBulkShareRequest } from "../vault/models/request/cipher-bulk-share.request";
import { CipherCollectionsRequest } from "../vault/models/request/cipher-collections.request";
import { CipherCreateRequest } from "../vault/models/request/cipher-create.request";
@@ -614,12 +615,19 @@ export class ApiService implements ApiServiceAbstraction {
}
async putRestoreManyCiphers(
request: CipherBulkDeleteRequest
request: CipherBulkRestoreRequest
): Promise<ListResponse<CipherResponse>> {
const r = await this.send("PUT", "/ciphers/restore", request, true, true);
return new ListResponse<CipherResponse>(r, CipherResponse);
}
async putRestoreManyCiphersAdmin(
request: CipherBulkRestoreRequest
): Promise<ListResponse<CipherResponse>> {
const r = await this.send("PUT", "/ciphers/restore-admin", request, true, true);
return new ListResponse<CipherResponse>(r, CipherResponse);
}
// Attachments APIs
async getAttachmentData(
@@ -881,7 +889,7 @@ export class ApiService implements ApiServiceAbstraction {
// Plan APIs
async getPlans(): Promise<ListResponse<PlanResponse>> {
const r = await this.send("GET", "/plans/", null, false, true);
const r = await this.send("GET", "/plans/all", null, false, true);
return new ListResponse(r, PlanResponse);
}

View File

@@ -57,10 +57,10 @@ describe("deviceCryptoService", () => {
let makeDeviceKeySpy: jest.SpyInstance;
beforeEach(() => {
mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockRandomBytes);
existingDeviceKey = new SymmetricCryptoKey(
new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray
new Uint8Array(deviceKeyBytesLength) as CsprngArray
) as DeviceKey;
stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey");
@@ -97,7 +97,7 @@ describe("deviceCryptoService", () => {
describe("makeDeviceKey", () => {
it("creates a new non-null 64 byte device key, securely stores it, and returns it", async () => {
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
const mockRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
const cryptoFuncSvcRandomBytesSpy = jest
.spyOn(cryptoFunctionService, "randomBytes")
@@ -128,9 +128,9 @@ describe("deviceCryptoService", () => {
let mockUserSymKey: SymmetricCryptoKey;
const deviceRsaKeyLength = 2048;
let mockDeviceRsaKeyPair: [ArrayBuffer, ArrayBuffer];
let mockDevicePrivateKey: ArrayBuffer;
let mockDevicePublicKey: ArrayBuffer;
let mockDeviceRsaKeyPair: [Uint8Array, Uint8Array];
let mockDevicePrivateKey: Uint8Array;
let mockDevicePublicKey: Uint8Array;
let mockDevicePublicKeyEncryptedUserSymKey: EncString;
let mockUserSymKeyEncryptedDevicePublicKey: EncString;
let mockDeviceKeyEncryptedDevicePrivateKey: EncString;
@@ -156,15 +156,15 @@ describe("deviceCryptoService", () => {
beforeEach(() => {
// Setup all spies and default return values for the happy path
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray;
mockDeviceKeyRandomBytes = new Uint8Array(deviceKeyBytesLength) as CsprngArray;
mockDeviceKey = new SymmetricCryptoKey(mockDeviceKeyRandomBytes) as DeviceKey;
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength).buffer as CsprngArray;
mockUserSymKeyRandomBytes = new Uint8Array(userSymKeyBytesLength) as CsprngArray;
mockUserSymKey = new SymmetricCryptoKey(mockUserSymKeyRandomBytes);
mockDeviceRsaKeyPair = [
new ArrayBuffer(deviceRsaKeyLength),
new ArrayBuffer(deviceRsaKeyLength),
new Uint8Array(deviceRsaKeyLength),
new Uint8Array(deviceRsaKeyLength),
];
mockDevicePublicKey = mockDeviceRsaKeyPair[0];

View File

@@ -206,6 +206,19 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
return new ListResponse(r, OrganizationUserBulkResponse);
}
async putOrganizationUserBulkEnableSecretsManager(
organizationId: string,
ids: string[]
): Promise<void> {
await this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/users/enable-secrets-manager",
new OrganizationUserBulkRequest(ids),
true,
false
);
}
putOrganizationUser(
organizationId: string,
id: string,

View File

@@ -162,7 +162,7 @@ export class TotpService implements TotpServiceAbstraction {
timeBytes: Uint8Array,
alg: "sha1" | "sha256" | "sha512"
) {
const signature = await this.cryptoFunctionService.hmac(timeBytes.buffer, keyBytes.buffer, alg);
const signature = await this.cryptoFunctionService.hmac(timeBytes, keyBytes, alg);
return new Uint8Array(signature);
}
}

View File

@@ -13,7 +13,7 @@ export class SendView implements View {
accessId: string = null;
name: string = null;
notes: string = null;
key: ArrayBuffer;
key: Uint8Array;
cryptoKey: SymmetricCryptoKey;
type: SendType = null;
text = new SendTextView();
@@ -82,7 +82,7 @@ export class SendView implements View {
}
return Object.assign(new SendView(), json, {
key: Utils.fromB64ToArray(json.key)?.buffer,
key: Utils.fromB64ToArray(json.key),
cryptoKey: SymmetricCryptoKey.fromJSON(json.cryptoKey),
text: SendTextView.fromJSON(json.text),
file: SendFileView.fromJSON(json.file),

View File

@@ -241,7 +241,7 @@ export class SendService implements InternalSendServiceAbstraction {
key: SymmetricCryptoKey
): Promise<[EncString, EncArrayBuffer]> {
const encFileName = await this.cryptoService.encrypt(fileName, key);
const encFileData = await this.cryptoService.encryptToBytes(data, key);
const encFileData = await this.cryptoService.encryptToBytes(new Uint8Array(data), key);
return [encFileName, encFileData];
}

View File

@@ -4,6 +4,6 @@ import { Opaque } from "type-fest";
// represents an array or string value generated from a
// cryptographic secure pseudorandom number generator (CSPRNG)
type CsprngArray = Opaque<ArrayBuffer, "CSPRNG">;
type CsprngArray = Opaque<Uint8Array, "CSPRNG">;
type CsprngString = Opaque<string, "CSPRNG">;

View File

@@ -33,8 +33,8 @@ export abstract class CipherService {
updateLastUsedDate: (id: string) => Promise<void>;
updateLastLaunchedDate: (id: string) => Promise<void>;
saveNeverDomain: (domain: string) => Promise<void>;
createWithServer: (cipher: Cipher) => Promise<Cipher>;
updateWithServer: (cipher: Cipher) => Promise<any>;
createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise<any>;
updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise<any>;
shareWithServer: (
cipher: CipherView,
organizationId: string,
@@ -76,5 +76,9 @@ export abstract class CipherService {
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[]
) => Promise<any>;
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
restoreManyWithServer: (ids: string[]) => Promise<any>;
restoreManyWithServer: (
ids: string[],
organizationId?: string,
asAdmin?: boolean
) => Promise<void>;
}

View File

@@ -1,7 +1,9 @@
export class CipherBulkRestoreRequest {
ids: string[];
organizationId: string;
constructor(ids: string[]) {
constructor(ids: string[], organizationId?: string) {
this.ids = ids == null ? [] : ids;
this.organizationId = organizationId;
}
}

View File

@@ -1,48 +1,106 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock, mockReset } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { UriMatchType, FieldType } from "../../enums";
import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { StateService } from "../../platform/abstractions/state.service";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data";
import { Cipher } from "../models/domain/cipher";
import { CipherCreateRequest } from "../models/request/cipher-create.request";
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
import { CipherRequest } from "../models/request/cipher.request";
import { CipherService } from "./cipher.service";
const ENCRYPTED_TEXT = "This data has been encrypted";
const ENCRYPTED_BYTES = Substitute.for<EncArrayBuffer>();
const cipherData: CipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Login,
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
reprompt: CipherRepromptType.None,
login: {
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "EncryptedString",
autofillOnPageLoad: false,
},
passwordHistory: [{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }],
attachments: [
{
id: "a1",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
{
id: "a2",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
],
fields: [
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Text,
linkedId: null,
},
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Hidden,
linkedId: null,
},
],
};
describe("Cipher Service", () => {
let cryptoService: SubstituteOf<CryptoService>;
let stateService: SubstituteOf<StateService>;
let settingsService: SubstituteOf<SettingsService>;
let apiService: SubstituteOf<ApiService>;
let cipherFileUploadService: SubstituteOf<CipherFileUploadService>;
let i18nService: SubstituteOf<I18nService>;
let searchService: SubstituteOf<SearchService>;
let encryptService: SubstituteOf<EncryptService>;
const cryptoService = mock<CryptoService>();
const stateService = mock<StateService>();
const settingsService = mock<SettingsService>();
const apiService = mock<ApiService>();
const cipherFileUploadService = mock<CipherFileUploadService>();
const i18nService = mock<I18nService>();
const searchService = mock<SearchService>();
const encryptService = mock<EncryptService>();
let cipherService: CipherService;
let cipherObj: Cipher;
beforeEach(() => {
cryptoService = Substitute.for<CryptoService>();
stateService = Substitute.for<StateService>();
settingsService = Substitute.for<SettingsService>();
apiService = Substitute.for<ApiService>();
cipherFileUploadService = Substitute.for<CipherFileUploadService>();
i18nService = Substitute.for<I18nService>();
searchService = Substitute.for<SearchService>();
encryptService = Substitute.for<EncryptService>();
cryptoService.encryptToBytes(Arg.any(), Arg.any()).resolves(ENCRYPTED_BYTES);
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(new EncString(ENCRYPTED_TEXT));
mockReset(apiService);
mockReset(cryptoService);
mockReset(stateService);
mockReset(settingsService);
mockReset(cipherFileUploadService);
mockReset(i18nService);
mockReset(searchService);
mockReset(encryptService);
cipherService = new CipherService(
cryptoService,
@@ -54,17 +112,97 @@ describe("Cipher Service", () => {
encryptService,
cipherFileUploadService
);
cipherObj = new Cipher(cipherData);
});
describe("saveAttachmentRawWithServer()", () => {
it("should upload encrypted file contents with save attachments", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10).buffer;
cryptoService.getOrgKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
);
cryptoService.makeEncKey.mockReturnValue(
Promise.resolve<any>(new SymmetricCryptoKey(new Uint8Array(32)))
);
const spy = jest.spyOn(cipherFileUploadService, "upload");
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
expect(spy).toHaveBeenCalled();
});
});
it("attachments upload encrypted file contents", async () => {
const fileName = "filename";
const fileData = new Uint8Array(10).buffer;
cryptoService.getOrgKey(Arg.any()).resolves(new SymmetricCryptoKey(new Uint8Array(32).buffer));
describe("createWithServer()", () => {
it("should call apiService.postCipherAdmin when orgAdmin param is true", async () => {
const spy = jest
.spyOn(apiService, "postCipherAdmin")
.mockImplementation(() => Promise.resolve<any>(cipherObj));
cipherService.createWithServer(cipherObj, true);
const expectedObj = new CipherCreateRequest(cipherObj);
await cipherService.saveAttachmentRawWithServer(new Cipher(), fileName, fileData);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
cipherFileUploadService
.received(1)
.upload(Arg.any(), Arg.any(), ENCRYPTED_BYTES, Arg.any(), Arg.any());
it("should call apiService.postCipherCreate if collectionsIds != null", async () => {
cipherObj.collectionIds = ["123"];
const spy = jest
.spyOn(apiService, "postCipherCreate")
.mockImplementation(() => Promise.resolve<any>(cipherObj));
cipherService.createWithServer(cipherObj);
const expectedObj = new CipherCreateRequest(cipherObj);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => {
const spy = jest
.spyOn(apiService, "postCipher")
.mockImplementation(() => Promise.resolve<any>(cipherObj));
cipherService.createWithServer(cipherObj);
const expectedObj = new CipherRequest(cipherObj);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(expectedObj);
});
});
describe("updateWithServer()", () => {
it("should call apiService.putCipherAdmin when orgAdmin and isNotClone params are true", async () => {
const spy = jest
.spyOn(apiService, "putCipherAdmin")
.mockImplementation(() => Promise.resolve<any>(cipherObj));
cipherService.updateWithServer(cipherObj, true, true);
const expectedObj = new CipherRequest(cipherObj);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj);
});
it("should call apiService.putCipher if cipher.edit is true", async () => {
cipherObj.edit = true;
const spy = jest
.spyOn(apiService, "putCipher")
.mockImplementation(() => Promise.resolve<any>(cipherObj));
cipherService.updateWithServer(cipherObj);
const expectedObj = new CipherRequest(cipherObj);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj);
});
it("should call apiService.putPartialCipher when orgAdmin, isNotClone, and edit are false", async () => {
cipherObj.edit = false;
const spy = jest
.spyOn(apiService, "putPartialCipher")
.mockImplementation(() => Promise.resolve<any>(cipherObj));
cipherService.updateWithServer(cipherObj);
const expectedObj = new CipherPartialRequest(cipherObj);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(cipherObj.id, expectedObj);
});
});
});

View File

@@ -520,9 +520,12 @@ export class CipherService implements CipherServiceAbstraction {
await this.stateService.setNeverDomains(domains);
}
async createWithServer(cipher: Cipher): Promise<any> {
async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<any> {
let response: CipherResponse;
if (cipher.collectionIds != null) {
if (orgAdmin) {
const request = new CipherCreateRequest(cipher);
response = await this.apiService.postCipherAdmin(request);
} else if (cipher.collectionIds != null) {
const request = new CipherCreateRequest(cipher);
response = await this.apiService.postCipherCreate(request);
} else {
@@ -535,9 +538,12 @@ export class CipherService implements CipherServiceAbstraction {
await this.upsert(data);
}
async updateWithServer(cipher: Cipher): Promise<any> {
async updateWithServer(cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean): Promise<any> {
let response: CipherResponse;
if (cipher.edit) {
if (orgAdmin && isNotClone) {
const request = new CipherRequest(cipher);
response = await this.apiService.putCipherAdmin(cipher.id, request);
} else if (cipher.edit) {
const request = new CipherRequest(cipher);
response = await this.apiService.putCipher(cipher.id, request);
} else {
@@ -638,7 +644,7 @@ export class CipherService implements CipherServiceAbstraction {
const encFileName = await this.cryptoService.encrypt(filename, key);
const dataEncKey = await this.cryptoService.makeEncKey(key);
const encData = await this.cryptoService.encryptToBytes(data, dataEncKey[0]);
const encData = await this.cryptoService.encryptToBytes(new Uint8Array(data), dataEncKey[0]);
const response = await this.cipherFileUploadService.upload(
cipher,
@@ -741,10 +747,11 @@ export class CipherService implements CipherServiceAbstraction {
}
async deleteManyWithServer(ids: string[], asAdmin = false): Promise<any> {
const request = new CipherBulkDeleteRequest(ids);
if (asAdmin) {
await this.apiService.deleteManyCiphersAdmin(new CipherBulkDeleteRequest(ids));
await this.apiService.deleteManyCiphersAdmin(request);
} else {
await this.apiService.deleteManyCiphers(new CipherBulkDeleteRequest(ids));
await this.apiService.deleteManyCiphers(request);
}
await this.delete(ids);
}
@@ -880,10 +887,11 @@ export class CipherService implements CipherServiceAbstraction {
}
async softDeleteManyWithServer(ids: string[], asAdmin = false): Promise<any> {
const request = new CipherBulkDeleteRequest(ids);
if (asAdmin) {
await this.apiService.putDeleteManyCiphersAdmin(new CipherBulkDeleteRequest(ids));
await this.apiService.putDeleteManyCiphersAdmin(request);
} else {
await this.apiService.putDeleteManyCiphers(new CipherBulkDeleteRequest(ids));
await this.apiService.putDeleteManyCiphers(request);
}
await this.softDelete(ids);
@@ -916,14 +924,30 @@ export class CipherService implements CipherServiceAbstraction {
}
async restoreWithServer(id: string, asAdmin = false): Promise<any> {
const response = asAdmin
? await this.apiService.putRestoreCipherAdmin(id)
: await this.apiService.putRestoreCipher(id);
let response;
if (asAdmin) {
response = await this.apiService.putRestoreCipherAdmin(id);
} else {
response = await this.apiService.putRestoreCipher(id);
}
await this.restore({ id: id, revisionDate: response.revisionDate });
}
async restoreManyWithServer(ids: string[]): Promise<any> {
const response = await this.apiService.putRestoreManyCiphers(new CipherBulkRestoreRequest(ids));
async restoreManyWithServer(
ids: string[],
organizationId: string = null,
asAdmin = false
): Promise<void> {
let response;
if (asAdmin) {
const request = new CipherBulkRestoreRequest(ids, organizationId);
response = await this.apiService.putRestoreManyCiphersAdmin(request);
} else {
const request = new CipherBulkRestoreRequest(ids);
response = await this.apiService.putRestoreManyCiphers(request);
}
const restores: { id: string; revisionDate: string }[] = [];
for (const cipher of response.data) {
restores.push({ id: cipher.id, revisionDate: cipher.revisionDate });

View File

@@ -1,6 +1,6 @@
import { ApiService } from "../../../abstractions/api.service";
import { SettingsService } from "../../../abstractions/settings.service";
import { InternalOrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
@@ -55,7 +55,7 @@ export class SyncService implements SyncServiceAbstraction {
private stateService: StateService,
private providerService: ProviderService,
private folderApiService: FolderApiServiceAbstraction,
private organizationService: InternalOrganizationService,
private organizationService: InternalOrganizationServiceAbstraction,
private sendApiService: SendApiService,
private logoutCallback: (expired: boolean) => Promise<void>
) {}