1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 01:23:24 +00:00

Merge remote-tracking branch 'origin/main' into ps/pm-15333/portable-desktop

This commit is contained in:
Justin Baur
2025-01-21 16:44:39 -05:00
1301 changed files with 52111 additions and 36450 deletions

View File

@@ -1,9 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
CollectionRequest,
CollectionAccessDetailsResponse,
CollectionDetailsResponse,
CollectionRequest,
CollectionResponse,
} from "@bitwarden/admin-console/common";
@@ -95,7 +95,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
import { PlanResponse } from "../billing/models/response/plan.response";
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
import { TaxRateResponse } from "../billing/models/response/tax-rate.response";
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
import { EventRequest } from "../models/request/event.request";
import { KdfRequest } from "../models/request/kdf.request";
@@ -137,7 +136,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher
*/
export abstract class ApiService {
send: (
method: "GET" | "POST" | "PUT" | "DELETE",
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: string,
body: any,
authed: boolean,
@@ -376,7 +375,6 @@ export abstract class ApiService {
): Promise<OrganizationConnectionResponse<TConfig>>;
deleteOrganizationConnection: (id: string) => Promise<void>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;

View File

@@ -1,7 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request";
import { OrganizationDomainSsoDetailsResponse } from "./responses/organization-domain-sso-details.response";

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { BaseResponse } from "../../../../models/response/base.response";
export class VerifiedOrganizationDomainSsoDetailsResponse extends BaseResponse {
organizationName: string;

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response";
import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request";
import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request";
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
@@ -13,6 +11,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
import { PaymentRequest } from "../../../billing/models/request/payment.request";
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response";
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";
@@ -53,11 +52,11 @@ export class OrganizationApiServiceAbstraction {
updatePasswordManagerSeats: (
id: string,
request: OrganizationSubscriptionUpdateRequest,
) => Promise<void>;
) => Promise<ProfileOrganizationResponse>;
updateSecretsManagerSubscription: (
id: string,
request: OrganizationSmSubscriptionUpdateRequest,
) => Promise<void>;
) => Promise<ProfileOrganizationResponse>;
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;

View File

@@ -17,7 +17,7 @@ export function canAccessSettingsTab(org: Organization): boolean {
org.canManageSso ||
org.canManageScim ||
org.canAccessImport ||
org.canAccessExport(false) || // Feature flag value doesn't matter here, providers will have access to this group anyway
org.canAccessExport ||
org.canManageDeviceApprovals
);
}

View File

@@ -17,7 +17,7 @@ export function canAccessSettingsTab(org: Organization): boolean {
org.canManageSso ||
org.canManageScim ||
org.canAccessImport ||
org.canAccessExport(false) || // Feature flag value doesn't matter here, providers will have access to this group anyway
org.canAccessExport ||
org.canManageDeviceApprovals
);
}

View File

@@ -182,11 +182,7 @@ export class Organization {
);
}
canAccessExport(removeProviderExport: boolean) {
if (!removeProviderExport && this.isProviderUser) {
return true;
}
get canAccessExport() {
return (
this.isMember &&
(this.type === OrganizationUserType.Owner ||

View File

@@ -1,14 +1,13 @@
import { mock } from "jest-mock-extended";
import { lastValueFrom } from "rxjs";
import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ApiService } from "../../../abstractions/api.service";
import { ListResponse } from "../../../models/response/list.response";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response";
import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response";
import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
import { OrgDomainApiService } from "./org-domain-api.service";
import { OrgDomainService } from "./org-domain.service";

View File

@@ -1,6 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response";
import { ApiService } from "../../../abstractions/api.service";
import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request";
@@ -14,6 +13,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
import { PaymentRequest } from "../../../billing/models/request/payment.request";
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response";
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";
@@ -161,27 +161,29 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
async updatePasswordManagerSeats(
id: string,
request: OrganizationSubscriptionUpdateRequest,
): Promise<void> {
return this.apiService.send(
): Promise<ProfileOrganizationResponse> {
const r = await this.apiService.send(
"POST",
"/organizations/" + id + "/subscription",
request,
true,
false,
true,
);
return new ProfileOrganizationResponse(r);
}
async updateSecretsManagerSubscription(
id: string,
request: OrganizationSmSubscriptionUpdateRequest,
): Promise<void> {
return this.apiService.send(
): Promise<ProfileOrganizationResponse> {
const r = await this.apiService.send(
"POST",
"/organizations/" + id + "/sm-subscription",
request,
true,
false,
true,
);
return new ProfileOrganizationResponse(r);
}
async updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse> {

View File

@@ -1,7 +1,6 @@
import { Jsonify } from "type-fest";
import { ORGANIZATIONS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
import { ORGANIZATIONS_DISK, UserKeyDefinition } from "../../../platform/state";
import { OrganizationData } from "../../models/data/organization.data";
/**

View File

@@ -36,4 +36,10 @@ export abstract class DevicesApiServiceAbstraction {
* @param deviceIdentifier - current device identifier
*/
postDeviceTrustLoss: (deviceIdentifier: string) => Promise<void>;
/**
* Deactivates a device
* @param deviceId - The device ID
*/
deactivateDevice: (deviceId: string) => Promise<void>;
}

View File

@@ -1,17 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { DeviceResponse } from "./responses/device.response";
import { DeviceView } from "./views/device.view";
export abstract class DevicesServiceAbstraction {
getDevices$: () => Observable<Array<DeviceView>>;
getDeviceByIdentifier$: (deviceIdentifier: string) => Observable<DeviceView>;
isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable<boolean>;
updateTrustedDeviceKeys$: (
abstract getDevices$(): Observable<Array<DeviceView>>;
abstract getDeviceByIdentifier$(deviceIdentifier: string): Observable<DeviceView>;
abstract isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable<boolean>;
abstract updateTrustedDeviceKeys$(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string,
) => Observable<DeviceView>;
): Observable<DeviceView>;
abstract deactivateDevice$(deviceId: string): Observable<void>;
abstract getCurrentDevice$(): Observable<DeviceResponse>;
}

View File

@@ -1,6 +1,11 @@
import { DeviceType } from "../../../../enums";
import { BaseResponse } from "../../../../models/response/base.response";
export interface DevicePendingAuthRequest {
id: string;
creationDate: string;
}
export class DeviceResponse extends BaseResponse {
id: string;
userId: string;
@@ -9,6 +14,9 @@ export class DeviceResponse extends BaseResponse {
type: DeviceType;
creationDate: string;
revisionDate: string;
isTrusted: boolean;
devicePendingAuthRequest: DevicePendingAuthRequest | null;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
@@ -18,5 +26,7 @@ export class DeviceResponse extends BaseResponse {
this.type = this.getResponseProperty("Type");
this.creationDate = this.getResponseProperty("CreationDate");
this.revisionDate = this.getResponseProperty("RevisionDate");
this.isTrusted = this.getResponseProperty("IsTrusted");
this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest");
}
}

View File

@@ -12,6 +12,7 @@ export class DeviceView implements View {
type: DeviceType;
creationDate: string;
revisionDate: string;
response: DeviceResponse;
constructor(deviceResponse: DeviceResponse) {
Object.assign(this, deviceResponse);

View File

@@ -8,8 +8,8 @@ export class UpdateDevicesTrustRequest extends SecretVerificationRequest {
}
export class DeviceKeysUpdateRequest {
encryptedPublicKey: string;
encryptedUserKey: string;
encryptedPublicKey: string | undefined;
encryptedUserKey: string | undefined;
}
export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest {

View File

@@ -1,8 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { RotateableKeySet } from "../../../../../auth/src/common/models";
import { EncString } from "../../../platform/models/domain/enc-string";
export class WebauthnRotateCredentialRequest {
id: string;

View File

@@ -9,6 +9,8 @@ import {
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { firstValueFrom } from "rxjs";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { AuthRequestServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { NotificationType } from "../../enums";
import {

View File

@@ -1,6 +1,8 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import {
FakeAccountService,

View File

@@ -11,6 +11,8 @@ import {
switchMap,
} from "rxjs";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { StateService } from "../../platform/abstractions/state.service";

View File

@@ -4,6 +4,8 @@ import { firstValueFrom, map, Observable } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
@@ -335,6 +337,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
);
return new SymmetricCryptoKey(userKey) as UserKey;
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// If either decryption effort fails, we want to remove the device key
this.logService.error("Failed to decrypt using device key. Removing device key.");

View File

@@ -3,7 +3,11 @@ import { BehaviorSubject, of } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState } from "../../../spec/fake-state";

View File

@@ -0,0 +1,100 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "../../abstractions/api.service";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { DevicesApiServiceImplementation } from "./devices-api.service.implementation";
describe("DevicesApiServiceImplementation", () => {
let devicesApiService: DevicesApiServiceImplementation;
let apiService: MockProxy<ApiService>;
beforeEach(() => {
apiService = mock<ApiService>();
devicesApiService = new DevicesApiServiceImplementation(apiService);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("getKnownDevice", () => {
it("calls api with correct parameters", async () => {
const email = "test@example.com";
const deviceIdentifier = "device123";
apiService.send.mockResolvedValue(true);
const result = await devicesApiService.getKnownDevice(email, deviceIdentifier);
expect(result).toBe(true);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
"/devices/knowndevice",
null,
false,
true,
null,
expect.any(Function),
);
});
});
describe("getDeviceByIdentifier", () => {
it("returns device response", async () => {
const deviceIdentifier = "device123";
const mockResponse = { id: "123", name: "Test Device" };
apiService.send.mockResolvedValue(mockResponse);
const result = await devicesApiService.getDeviceByIdentifier(deviceIdentifier);
expect(result).toBeInstanceOf(DeviceResponse);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`/devices/identifier/${deviceIdentifier}`,
null,
true,
true,
);
});
});
describe("updateTrustedDeviceKeys", () => {
it("updates device keys and returns device response", async () => {
const deviceIdentifier = "device123";
const publicKeyEncrypted = "encryptedPublicKey";
const userKeyEncrypted = "encryptedUserKey";
const deviceKeyEncrypted = "encryptedDeviceKey";
const mockResponse = { id: "123", name: "Test Device" };
apiService.send.mockResolvedValue(mockResponse);
const result = await devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
publicKeyEncrypted,
userKeyEncrypted,
deviceKeyEncrypted,
);
expect(result).toBeInstanceOf(DeviceResponse);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`/devices/${deviceIdentifier}/keys`,
{
encryptedPrivateKey: deviceKeyEncrypted,
encryptedPublicKey: userKeyEncrypted,
encryptedUserKey: publicKeyEncrypted,
},
true,
true,
);
});
});
describe("error handling", () => {
it("propagates api errors", async () => {
const error = new Error("API Error");
apiService.send.mockRejectedValue(error);
await expect(devicesApiService.getDevices()).rejects.toThrow("API Error");
});
});
});

View File

@@ -117,4 +117,8 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac
},
);
}
async deactivateDevice(deviceId: string): Promise<void> {
await this.apiService.send("POST", `/devices/${deviceId}/deactivate`, null, true, false);
}
}

View File

@@ -1,6 +1,7 @@
import { Observable, defer, map } from "rxjs";
import { ListResponse } from "../../../models/response/list.response";
import { AppIdService } from "../../../platform/abstractions/app-id.service";
import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction";
import { DeviceResponse } from "../../abstractions/devices/responses/device.response";
import { DeviceView } from "../../abstractions/devices/views/device.view";
@@ -15,7 +16,10 @@ import { DevicesApiServiceAbstraction } from "../../abstractions/devices-api.ser
* (i.e., promsise --> observables are cold until subscribed to)
*/
export class DevicesServiceImplementation implements DevicesServiceAbstraction {
constructor(private devicesApiService: DevicesApiServiceAbstraction) {}
constructor(
private devicesApiService: DevicesApiServiceAbstraction,
private appIdService: AppIdService,
) {}
/**
* @description Gets the list of all devices.
@@ -65,4 +69,21 @@ export class DevicesServiceImplementation implements DevicesServiceAbstraction {
),
).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse)));
}
/**
* @description Deactivates a device
*/
deactivateDevice$(deviceId: string): Observable<void> {
return defer(() => this.devicesApiService.deactivateDevice(deviceId));
}
/**
* @description Gets the current device.
*/
getCurrentDevice$(): Observable<DeviceResponse> {
return defer(async () => {
const deviceIdentifier = await this.appIdService.getAppId();
return this.devicesApiService.getDeviceByIdentifier(deviceIdentifier);
});
}
}

View File

@@ -1,5 +1,7 @@
import { mock } from "jest-mock-extended";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { ApiService } from "../../abstractions/api.service";

View File

@@ -2,10 +2,9 @@
// @ts-strict-ignore
import { firstValueFrom, map, Observable } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncryptionType } from "../../../platform/enums";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
@@ -180,10 +179,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
let decUserKey: Uint8Array;
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
decUserKey = await this.encryptService.decryptToBytes(
userKey,
masterKey,
"Content: User Key; Encrypting Key: Master Key",
);
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
const newKey = await this.keyGenerationService.stretchKey(masterKey);
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
decUserKey = await this.encryptService.decryptToBytes(
userKey,
newKey,
"Content: User Key; Encrypting Key: Stretched Master Key",
);
} else {
throw new Error("Unsupported encryption type.");
}

View File

@@ -2,13 +2,15 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { UserId } from "../../../../common/src/types/guid";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { UserId } from "../../types/guid";
import { Account, AccountInfo, AccountService } from "../abstractions/account.service";
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";

View File

@@ -6,10 +6,12 @@ import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { Utils } from "../../platform/misc/utils";
import { UserKey } from "../../types/key";

View File

@@ -7,14 +7,19 @@ import {
UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { KdfConfig, KeyService } from "@bitwarden/key-management";
import {
BiometricsService,
BiometricsStatus,
KdfConfig,
KeyService,
} from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { HashPurpose } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import { UserId } from "../../../types/guid";
@@ -36,10 +41,9 @@ describe("UserVerificationService", () => {
const userVerificationApiService = mock<UserVerificationApiServiceAbstraction>();
const userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
const pinService = mock<PinServiceAbstraction>();
const logService = mock<LogService>();
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
const platformUtilsService = mock<PlatformUtilsService>();
const kdfConfigService = mock<KdfConfigService>();
const biometricsService = mock<BiometricsService>();
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
@@ -56,10 +60,8 @@ describe("UserVerificationService", () => {
userVerificationApiService,
userDecryptionOptionsService,
pinService,
logService,
vaultTimeoutSettingsService,
platformUtilsService,
kdfConfigService,
biometricsService,
);
});
@@ -113,26 +115,15 @@ describe("UserVerificationService", () => {
);
test.each([
[true, true, true, true],
[true, true, true, false],
[true, true, false, false],
[false, true, false, true],
[false, false, false, false],
[false, false, true, false],
[false, false, false, true],
[true, BiometricsStatus.Available],
[false, BiometricsStatus.DesktopDisconnected],
[false, BiometricsStatus.HardwareUnavailable],
])(
"returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s",
async (
expectedReturn: boolean,
isBiometricsLockSet: boolean,
isBiometricsUserKeyStored: boolean,
platformSupportSecureStorage: boolean,
) => {
async (expectedReturn: boolean, biometricsStatus: BiometricsStatus) => {
setMasterPasswordAvailability(false);
setPinAvailability("DISABLED");
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet);
keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored);
platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage);
biometricsService.getBiometricsStatus.mockResolvedValue(biometricsStatus);
const result = await sut.getAvailableVerificationOptions("client");

View File

@@ -3,17 +3,19 @@
import { firstValueFrom, map } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import {
BiometricsService,
BiometricsStatus,
KdfConfigService,
KeyService,
} from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { HashPurpose } from "../../../platform/enums";
import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum";
import { UserId } from "../../../types/guid";
import { UserKey } from "../../../types/key";
import { AccountService } from "../../abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
@@ -47,10 +49,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinService: PinServiceAbstraction,
private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private kdfConfigService: KdfConfigService,
private biometricsService: BiometricsService,
) {}
async getAvailableVerificationOptions(
@@ -58,17 +58,13 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
): Promise<UserVerificationOptions> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (verificationType === "client") {
const [
userHasMasterPassword,
isPinDecryptionAvailable,
biometricsLockSet,
biometricsUserKeyStored,
] = await Promise.all([
this.hasMasterPasswordAndMasterKeyHash(userId),
this.pinService.isPinDecryptionAvailable(userId),
this.vaultTimeoutSettingsService.isBiometricLockSet(userId),
this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId),
]);
const [userHasMasterPassword, isPinDecryptionAvailable, biometricsStatus] = await Promise.all(
[
this.hasMasterPasswordAndMasterKeyHash(userId),
this.pinService.isPinDecryptionAvailable(userId),
this.biometricsService.getBiometricsStatus(),
],
);
// note: we do not need to check this.platformUtilsService.supportsBiometric() because
// we can just use the logic below which works for both desktop & the browser extension.
@@ -77,9 +73,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
client: {
masterPassword: userHasMasterPassword,
pin: isPinDecryptionAvailable,
biometrics:
biometricsLockSet &&
(biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()),
biometrics: biometricsStatus === BiometricsStatus.Available,
},
server: {
masterPassword: false,
@@ -169,6 +163,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
const request = new VerifyOTPRequest(verification.secret);
try {
await this.userVerificationApiService.postAccountVerifyOTP(request);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
@@ -227,6 +223,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
request.masterPasswordHash = serverKeyHash;
try {
policyOptions = await this.userVerificationApiService.postAccountVerifyPassword(request);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
}
@@ -253,17 +251,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
}
private async verifyUserByBiometrics(): Promise<boolean> {
let userKey: UserKey;
// Biometrics crashes and doesn't return a value if the user cancels the prompt
try {
userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric);
} catch (e) {
this.logService.error(`Biometrics User Verification failed: ${e.message}`);
// So, any failures should be treated as a failed verification
return false;
}
return userKey != null;
return this.biometricsService.authenticateWithBiometrics();
}
async requestOTP() {

View File

@@ -1,6 +1,8 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
@@ -8,6 +10,7 @@ import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-se
describe("DefaultDomainSettingsService", () => {
let domainSettingsService: DomainSettingsService;
let configService: MockProxy<ConfigService>;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
@@ -19,10 +22,13 @@ describe("DefaultDomainSettingsService", () => {
];
beforeEach(() => {
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
configService = mock<ConfigService>();
configService.getFeatureFlag$.mockImplementation(() => of(false));
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
domainSettingsService.blockedInteractionsUris$ = of({});
});
describe("getUrlEquivalentDomains", () => {

View File

@@ -1,13 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { map, Observable } from "rxjs";
import { map, Observable, switchMap, of } from "rxjs";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import {
NeverDomains,
EquivalentDomains,
UriMatchStrategySetting,
UriMatchStrategy,
} from "../../models/domain/domain-service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { Utils } from "../../platform/misc/utils";
import {
DOMAIN_SETTINGS_DISK,
@@ -23,10 +25,20 @@ const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", {
deserializer: (value: boolean) => value ?? true,
});
// Domain exclusion list for notifications
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
deserializer: (value: NeverDomains) => value ?? null,
});
// Domain exclusion list for content script injections
const BLOCKED_INTERACTIONS_URIS = new KeyDefinition(
DOMAIN_SETTINGS_DISK,
"blockedInteractionsUris",
{
deserializer: (value: NeverDomains) => value ?? {},
},
);
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
deserializer: (value: EquivalentDomains) => value ?? null,
clearOn: ["logout"],
@@ -41,15 +53,45 @@ const DEFAULT_URI_MATCH_STRATEGY = new UserKeyDefinition(
},
);
/**
* The Domain Settings service; provides client settings state for "active client view" URI concerns
*/
export abstract class DomainSettingsService {
/**
* Indicates if the favicons for ciphers' URIs should be shown instead of a placeholder
*/
showFavicons$: Observable<boolean>;
setShowFavicons: (newValue: boolean) => Promise<void>;
/**
* User-specified URIs for which the client notifications should not appear
*/
neverDomains$: Observable<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
/**
* User-specified URIs for which client content script injections should not occur, and the state
* of banner/notice visibility for those domains within the client
*/
blockedInteractionsUris$: Observable<NeverDomains>;
setBlockedInteractionsUris: (newValue: NeverDomains) => Promise<void>;
/**
* URIs which should be treated as equivalent to each other for various concerns (autofill, etc)
*/
equivalentDomains$: Observable<EquivalentDomains>;
setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise<void>;
/**
* User-specified default for URI-matching strategies (for example, when determining relevant
* ciphers for an active browser tab). Can be overridden by cipher-specific settings.
*/
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
/**
* Helper function for the common resolution of a given URL against equivalent domains
*/
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
}
@@ -60,19 +102,37 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
private neverDomainsState: GlobalState<NeverDomains>;
readonly neverDomains$: Observable<NeverDomains>;
private blockedInteractionsUrisState: GlobalState<NeverDomains>;
readonly blockedInteractionsUris$: Observable<NeverDomains>;
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
readonly equivalentDomains$: Observable<EquivalentDomains>;
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
constructor(private stateProvider: StateProvider) {
constructor(
private stateProvider: StateProvider,
private configService: ConfigService,
) {
this.showFaviconsState = this.stateProvider.getGlobal(SHOW_FAVICONS);
this.showFavicons$ = this.showFaviconsState.state$.pipe(map((x) => x ?? true));
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
// Needs to be global to prevent pre-login injections
this.blockedInteractionsUrisState = this.stateProvider.getGlobal(BLOCKED_INTERACTIONS_URIS);
this.blockedInteractionsUris$ = this.configService
.getFeatureFlag$(FeatureFlag.BlockBrowserInjectionsByDomain)
.pipe(
switchMap((featureIsEnabled) =>
featureIsEnabled ? this.blockedInteractionsUrisState.state$ : of({} as NeverDomains),
),
map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : {})),
);
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
@@ -90,6 +150,10 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
await this.neverDomainsState.update(() => newValue);
}
async setBlockedInteractionsUris(newValue: NeverDomains): Promise<void> {
await this.blockedInteractionsUrisState.update(() => newValue);
}
async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
}

View File

@@ -1,9 +1,6 @@
import {
normalizeExpiryYearFormat,
isCardExpired,
parseYearMonthExpiry,
} from "@bitwarden/common/autofill/utils";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CardView } from "../vault/models/view/card.view";
import { normalizeExpiryYearFormat, isCardExpired, parseYearMonthExpiry } from "./utils";
function getExpiryYearValueFormats(currentCentury: string) {
return [
@@ -86,12 +83,14 @@ function getCardExpiryDateValues() {
// `Date` months are zero-indexed, our expiry date month inputs are one-indexed
const currentMonth = currentDate.getMonth() + 1;
const currentDateLastMonth = new Date(currentDate.setMonth(-1));
return [
[null, null, false], // no month, no year
[undefined, undefined, false], // no month, no year, invalid values
["", "", false], // no month, no year, invalid values
["12", "agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", false], // invalid values
["0", `${currentYear}`, true], // invalid month
["0", `${currentYear}`, false], // invalid month
["0", `${currentYear - 1}`, true], // invalid 0 month
["00", `${currentYear + 1}`, false], // invalid 0 month
[`${currentMonth}`, "0000", true], // current month, in the year 2000
@@ -103,7 +102,7 @@ function getCardExpiryDateValues() {
[`${currentMonth + 36}`, `${currentYear - 1}`, true], // even though the month value would put the date 3 years into the future when calculated with `Date`, an explicit year in the past indicates the card is expired
[`${currentMonth}`, `${currentYear}`, false], // this year, this month (not expired until the month is over)
[`${currentMonth}`, `${currentYear}`.slice(-2), false], // This month, this year (not expired until the month is over)
[`${currentMonth - 1}`, `${currentYear}`, true], // last month
[`${currentDateLastMonth.getMonth() + 1}`, `${currentDateLastMonth.getFullYear()}`, true], // last month
[`${currentMonth - 1}`, `${currentYear + 1}`, false], // 11 months from now
];
}

View File

@@ -1,13 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CardView } from "../vault/models/view/card.view";
import {
DelimiterPatternExpression,
ExpiryFullYearPattern,
ExpiryFullYearPatternExpression,
IrrelevantExpiryCharactersPatternExpression,
MonthPatternExpression,
} from "@bitwarden/common/autofill/constants";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
} from "./constants";
type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`;
@@ -25,11 +24,11 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu
let expirationYear = yearInputIsEmpty ? null : `${yearInput}`;
// Exit early if year is already formatted correctly or empty
if (yearInputIsEmpty || /^[1-9]{1}\d{3}$/.test(expirationYear)) {
if (yearInputIsEmpty || (expirationYear && /^[1-9]{1}\d{3}$/.test(expirationYear))) {
return expirationYear as Year;
}
expirationYear = expirationYear
expirationYear = (expirationYear || "")
// For safety, because even input[type="number"] will allow decimals
.replace(/[^\d]/g, "")
// remove any leading zero padding (leave the last leading zero if it ends the string)
@@ -53,7 +52,7 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu
/**
* Takes a cipher card view and returns "true" if the month and year affirmativey indicate
* the card is expired.
* the card is expired. Uncertain cases return "false".
*
* @param {CardView} cipherCard
* @return {*} {boolean}
@@ -62,27 +61,38 @@ export function isCardExpired(cipherCard: CardView): boolean {
if (cipherCard) {
const { expMonth = null, expYear = null } = cipherCard;
if (!expYear) {
return false;
}
const now = new Date();
const normalizedYear = normalizeExpiryYearFormat(expYear);
const parsedYear = normalizedYear ? parseInt(normalizedYear, 10) : NaN;
// If the card year is before the current year, don't bother checking the month
if (normalizedYear && parseInt(normalizedYear, 10) < now.getFullYear()) {
const expiryYearIsBeforeCurrentYear = parsedYear < now.getFullYear();
const expiryYearIsAfterCurrentYear = parsedYear > now.getFullYear();
// If the expiry year is before the current year, skip checking the month, since it must be expired
if (normalizedYear && expiryYearIsBeforeCurrentYear) {
return true;
}
// If the expiry year is after the current year, skip checking the month, since it cannot be expired
if (normalizedYear && expiryYearIsAfterCurrentYear) {
return false;
}
if (normalizedYear && expMonth) {
const parsedMonthInteger = parseInt(expMonth, 10);
const parsedMonthIsValid = parsedMonthInteger && !isNaN(parsedMonthInteger);
const parsedMonth = isNaN(parsedMonthInteger)
? 0
: // Add a month floor of 0 to protect against an invalid low month value of "0" or negative integers
Math.max(
// `Date` months are zero-indexed
parsedMonthInteger - 1,
0,
);
// If the parsed month value is 0, we don't know when the expiry passes this year, so do not treat it as expired
if (!parsedMonthIsValid) {
return false;
}
const parsedYear = parseInt(normalizedYear, 10);
// `Date` months are zero-indexed
const parsedMonth = parsedMonthInteger - 1;
// First day of the next month
const cardExpiry = new Date(parsedYear, parsedMonth + 1, 1);
@@ -250,13 +260,18 @@ function parseNonDelimitedYearMonthExpiry(dateInput: string): [string | null, st
parsedMonth = dateInput.slice(-1);
const currentYear = new Date().getFullYear();
const normalizedParsedYear = parseInt(normalizeExpiryYearFormat(parsedYear), 10);
const normalizedParsedYearAlternative = parseInt(
normalizeExpiryYearFormat(dateInput.slice(-2)),
10,
);
const normalizedYearFormat = normalizeExpiryYearFormat(parsedYear);
const normalizedParsedYear = normalizedYearFormat && parseInt(normalizedYearFormat, 10);
const normalizedExpiryYearFormat = normalizeExpiryYearFormat(dateInput.slice(-2));
const normalizedParsedYearAlternative =
normalizedExpiryYearFormat && parseInt(normalizedExpiryYearFormat, 10);
if (normalizedParsedYear < currentYear && normalizedParsedYearAlternative >= currentYear) {
if (
normalizedParsedYear &&
normalizedParsedYear < currentYear &&
normalizedParsedYearAlternative &&
normalizedParsedYearAlternative >= currentYear
) {
parsedYear = dateInput.slice(-2);
parsedMonth = dateInput.slice(0, 1);
}
@@ -288,17 +303,24 @@ export function parseYearMonthExpiry(combinedExpiryValue: string): [Year | null,
// If there is only one date part, no delimiter was found in the passed value
if (dateParts.length === 1) {
[parsedYear, parsedMonth] = parseNonDelimitedYearMonthExpiry(sanitizedFirstPart);
const [parsedNonDelimitedYear, parsedNonDelimitedMonth] =
parseNonDelimitedYearMonthExpiry(sanitizedFirstPart);
parsedYear = parsedNonDelimitedYear;
parsedMonth = parsedNonDelimitedMonth;
}
// There are multiple date parts
else {
[parsedYear, parsedMonth] = parseDelimitedYearMonthExpiry([
const [parsedDelimitedYear, parsedDelimitedMonth] = parseDelimitedYearMonthExpiry([
sanitizedFirstPart,
sanitizedSecondPart,
]);
parsedYear = parsedDelimitedYear;
parsedMonth = parsedDelimitedMonth;
}
const normalizedParsedYear = normalizeExpiryYearFormat(parsedYear);
const normalizedParsedYear = parsedYear ? normalizeExpiryYearFormat(parsedYear) : null;
const normalizedParsedMonth = parsedMonth?.replace(/^0+/, "").slice(0, 2);
// Set "empty" values to null

View File

@@ -3,7 +3,7 @@
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "@bitwarden/common/billing/models/response/billing.response";
} from "../../models/response/billing.response";
export class AccountBillingApiServiceAbstraction {
getBillingInvoices: (status?: string, startAfter?: string) => Promise<BillingInvoiceResponse[]>;

View File

@@ -11,27 +11,32 @@ export type BillingAccountProfile = {
export abstract class BillingAccountProfileStateService {
/**
* Emits `true` when the active user's account has been granted premium from any of the
* Emits `true` when the user's account has been granted premium from any of the
* organizations it is a member of. Otherwise, emits `false`
*/
hasPremiumFromAnyOrganization$: Observable<boolean>;
abstract hasPremiumFromAnyOrganization$(userId: UserId): Observable<boolean>;
/**
* Emits `true` when the active user's account has an active premium subscription at the
* Emits `true` when the user's account has an active premium subscription at the
* individual user level
*/
hasPremiumPersonally$: Observable<boolean>;
abstract hasPremiumPersonally$(userId: UserId): Observable<boolean>;
/**
* Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true`
*/
hasPremiumFromAnySource$: Observable<boolean>;
abstract hasPremiumFromAnySource$(userId: UserId): Observable<boolean>;
/**
* Sets the active user's premium status fields upon every full sync, either from their personal
* Emits `true` when the subscription menu item should be shown in navigation.
* This is hidden for organizations that provide premium, except if the user has premium personally
* or has a billing history.
*/
abstract canViewSubscription$(userId: UserId): Observable<boolean>;
/**
* Sets the user's premium status fields upon every full sync, either from their personal
* subscription to premium, or an organization they're a part of that grants them premium.
* @param hasPremiumPersonally
* @param hasPremiumFromAnyOrganization
*/
abstract setHasPremium(
hasPremiumPersonally: boolean,

View File

@@ -1,19 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response";
import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response";
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
import { PlanResponse } from "../../billing/models/response/plan.response";
import { ListResponse } from "../../models/response/list.response";
import { PaymentMethodType } from "../enums";
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request";
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request";
import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request";
import { InvoicesResponse } from "../models/response/invoices.response";
import { PaymentMethodResponse } from "../models/response/payment-method.response";
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
export abstract class BillingApiServiceAbstraction {
@@ -74,4 +75,9 @@ export abstract class BillingApiServiceAbstraction {
organizationId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
restartSubscription: (
organizationId: string,
request: OrganizationCreateRequest,
) => Promise<void>;
}

View File

@@ -1,11 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { InitiationPath } from "../../models/request/reference-event.request";
import { PaymentMethodType, PlanType } from "../enums";
import { BillingSourceResponse } from "../models/response/billing.response";
import { PaymentSourceResponse } from "../models/response/payment-source.response";
export type OrganizationInformation = {
name: string;
@@ -57,4 +57,9 @@ export abstract class OrganizationBillingServiceAbstraction {
) => Promise<OrganizationResponse>;
startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
restartSubscription: (
organizationId: string,
subscription: SubscriptionInformation,
) => Promise<void>;
}

View File

@@ -3,7 +3,7 @@
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "@bitwarden/common/billing/models/response/billing.response";
} from "../../models/response/billing.response";
export class OrganizationBillingApiServiceAbstraction {
getBillingInvoices: (

View File

@@ -0,0 +1,18 @@
import { CountryListItem } from "../models/domain";
import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request";
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
export abstract class TaxServiceAbstraction {
abstract getCountries(): CountryListItem[];
abstract isCountrySupported(country: string): Promise<boolean>;
abstract previewIndividualInvoice(
request: PreviewIndividualInvoiceRequest,
): Promise<PreviewInvoiceResponse>;
abstract previewOrganizationInvoice(
request: PreviewOrganizationInvoiceRequest,
): Promise<PreviewInvoiceResponse>;
}

View File

@@ -0,0 +1,5 @@
export type CountryListItem = {
name: string;
value: string;
disabled: boolean;
};

View File

@@ -1,2 +1,3 @@
export * from "./bank-account";
export * from "./country-list-item";
export * from "./tax-information";

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
import { TaxInfoResponse } from "../response/tax-info.response";
export class TaxInformation {
country: string;

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information";
import { TaxInformation } from "../domain/tax-information";
import { TaxInfoUpdateRequest } from "./tax-info-update.request";
@@ -12,6 +12,10 @@ export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest {
state: string;
static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest {
if (!taxInformation) {
return null;
}
const request = new ExpandedTaxInfoUpdateRequest();
request.country = taxInformation.country;
request.postalCode = taxInformation.postalCode;

View File

@@ -0,0 +1,28 @@
// @ts-strict-ignore
export class PreviewIndividualInvoiceRequest {
passwordManager: PasswordManager;
taxInformation: TaxInformation;
constructor(passwordManager: PasswordManager, taxInformation: TaxInformation) {
this.passwordManager = passwordManager;
this.taxInformation = taxInformation;
}
}
class PasswordManager {
additionalStorage: number;
constructor(additionalStorage: number) {
this.additionalStorage = additionalStorage;
}
}
class TaxInformation {
postalCode: string;
country: string;
constructor(postalCode: string, country: string) {
this.postalCode = postalCode;
this.country = country;
}
}

View File

@@ -0,0 +1,54 @@
import { PlanType } from "../../enums";
export class PreviewOrganizationInvoiceRequest {
organizationId?: string;
passwordManager: PasswordManager;
secretsManager?: SecretsManager;
taxInformation: TaxInformation;
constructor(
passwordManager: PasswordManager,
taxInformation: TaxInformation,
organizationId?: string,
secretsManager?: SecretsManager,
) {
this.organizationId = organizationId;
this.passwordManager = passwordManager;
this.secretsManager = secretsManager;
this.taxInformation = taxInformation;
}
}
class PasswordManager {
plan: PlanType;
seats: number;
additionalStorage: number;
constructor(plan: PlanType, seats: number, additionalStorage: number) {
this.plan = plan;
this.seats = seats;
this.additionalStorage = additionalStorage;
}
}
class SecretsManager {
seats: number;
additionalMachineAccounts: number;
constructor(seats: number, additionalMachineAccounts: number) {
this.seats = seats;
this.additionalMachineAccounts = additionalMachineAccounts;
}
}
class TaxInformation {
postalCode: string;
country: string;
taxId: string;
constructor(postalCode: string, country: string, taxId: string) {
this.postalCode = postalCode;
this.country = country;
this.taxId = taxId;
}
}

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { PaymentMethodType } from "../../enums";
export class TokenizedPaymentSourceRequest {
type: PaymentMethodType;

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request";
import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request";
import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request";
export class UpdatePaymentMethodRequest {
paymentSource: TokenizedPaymentSourceRequest;

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { BaseResponse } from "../../../models/response/base.response";
export class InvoicesResponse extends BaseResponse {
invoices: InvoiceResponse[] = [];

View File

@@ -6,6 +6,11 @@ export class OrganizationBillingMetadataResponse extends BaseResponse {
isOnSecretsManagerStandalone: boolean;
isSubscriptionUnpaid: boolean;
hasSubscription: boolean;
hasOpenInvoice: boolean;
invoiceDueDate: Date | null;
invoiceCreatedDate: Date | null;
subPeriodEndDate: Date | null;
isSubscriptionCanceled: boolean;
constructor(response: any) {
super(response);
@@ -14,5 +19,15 @@ export class OrganizationBillingMetadataResponse extends BaseResponse {
this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone");
this.isSubscriptionUnpaid = this.getResponseProperty("IsSubscriptionUnpaid");
this.hasSubscription = this.getResponseProperty("HasSubscription");
this.hasOpenInvoice = this.getResponseProperty("HasOpenInvoice");
this.invoiceDueDate = this.parseDate(this.getResponseProperty("InvoiceDueDate"));
this.invoiceCreatedDate = this.parseDate(this.getResponseProperty("InvoiceCreatedDate"));
this.subPeriodEndDate = this.parseDate(this.getResponseProperty("SubPeriodEndDate"));
this.isSubscriptionCanceled = this.getResponseProperty("IsSubscriptionCanceled");
}
private parseDate(dateString: any): Date | null {
return dateString ? new Date(dateString) : null;
}
}

View File

@@ -1,5 +1,5 @@
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { BaseResponse } from "../../../models/response/base.response";
import { PaymentMethodType } from "../../enums";
export class PaymentSourceResponse extends BaseResponse {
type: PaymentMethodType;

View File

@@ -0,0 +1,16 @@
import { BaseResponse } from "../../../models/response/base.response";
export class PreviewInvoiceResponse extends BaseResponse {
effectiveTaxRate: number;
taxableBaseAmount: number;
taxAmount: number;
totalAmount: number;
constructor(response: any) {
super(response);
this.effectiveTaxRate = this.getResponseProperty("EffectiveTaxRate");
this.taxableBaseAmount = this.getResponseProperty("TaxableBaseAmount");
this.taxAmount = this.getResponseProperty("TaxAmount");
this.totalAmount = this.getResponseProperty("TotalAmount");
}
}

View File

@@ -1,9 +1,9 @@
import { ProviderType } from "@bitwarden/common/admin-console/enums";
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { SubscriptionSuspensionResponse } from "@bitwarden/common/billing/models/response/subscription-suspension.response";
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
import { ProviderType } from "../../../admin-console/enums";
import { BaseResponse } from "../../../models/response/base.response";
import { PlanType, ProductTierType } from "../../enums";
import { SubscriptionSuspensionResponse } from "./subscription-suspension.response";
import { TaxInfoResponse } from "./tax-info.response";
export class ProviderSubscriptionResponse extends BaseResponse {
status: string;

View File

@@ -1,4 +1,4 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { BaseResponse } from "../../../models/response/base.response";
export class SubscriptionSuspensionResponse extends BaseResponse {
suspensionDate: string;

View File

@@ -0,0 +1,28 @@
import { BaseResponse } from "../../../models/response/base.response";
export class TaxIdTypesResponse extends BaseResponse {
taxIdTypes: TaxIdTypeResponse[] = [];
constructor(response: any) {
super(response);
const taxIdTypes = this.getResponseProperty("TaxIdTypes");
if (taxIdTypes && taxIdTypes.length) {
this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t));
}
}
}
export class TaxIdTypeResponse extends BaseResponse {
code: string;
country: string;
description: string;
example: string;
constructor(response: any) {
super(response);
this.code = this.getResponseProperty("Code");
this.country = this.getResponseProperty("Country");
this.description = this.getResponseProperty("Description");
this.example = this.getResponseProperty("Example");
}
}

View File

@@ -1,18 +0,0 @@
import { BaseResponse } from "../../../models/response/base.response";
export class TaxRateResponse extends BaseResponse {
id: string;
country: string;
state: string;
postalCode: string;
rate: number;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.country = this.getResponseProperty("Country");
this.state = this.getResponseProperty("State");
this.postalCode = this.getResponseProperty("PostalCode");
this.rate = this.getResponseProperty("Rate");
}
}

View File

@@ -6,8 +6,11 @@ import {
FakeStateProvider,
FakeSingleUserState,
} from "../../../../spec";
import { ApiService } from "../../../abstractions/api.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { UserId } from "../../../types/guid";
import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service";
import { BillingHistoryResponse } from "../../models/response/billing-history.response";
import {
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
@@ -19,14 +22,26 @@ describe("BillingAccountProfileStateService", () => {
let sut: DefaultBillingAccountProfileStateService;
let userBillingAccountProfileState: FakeSingleUserState<BillingAccountProfile>;
let accountService: FakeAccountService;
let platformUtilsService: jest.Mocked<PlatformUtilsService>;
let apiService: jest.Mocked<ApiService>;
const userId = "fakeUserId" as UserId;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
platformUtilsService = {
isSelfHost: jest.fn(),
} as any;
apiService = {
getUserBillingHistory: jest.fn(),
} as any;
sut = new DefaultBillingAccountProfileStateService(stateProvider);
sut = new DefaultBillingAccountProfileStateService(
stateProvider,
platformUtilsService,
apiService,
);
userBillingAccountProfileState = stateProvider.singleUser.getFake(
userId,
@@ -45,7 +60,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: true,
});
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(true);
});
it("return false when they do not have premium from an organization", async () => {
@@ -54,13 +69,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false,
});
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
});
it("returns false when there is no active user", async () => {
await accountService.switchAccount(null);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false);
});
});
@@ -71,7 +80,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false,
});
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true);
});
it("returns false when the user does not have premium personally", async () => {
@@ -80,13 +89,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false,
});
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false);
});
it("returns false when there is no active user", async () => {
await accountService.switchAccount(null);
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false);
expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(false);
});
});
@@ -97,7 +100,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: false,
});
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
});
it("returns true when the user has premium from an organization", async () => {
@@ -106,7 +109,7 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: true,
});
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
});
it("returns true when they have premium personally AND from an organization", async () => {
@@ -115,23 +118,87 @@ describe("BillingAccountProfileStateService", () => {
hasPremiumFromAnyOrganization: true,
});
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
});
it("returns false when there is no active user", async () => {
await accountService.switchAccount(null);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
});
});
describe("setHasPremium", () => {
it("should update the active users state when called", async () => {
it("should update the user's state when called", async () => {
await sut.setHasPremium(true, false, userId);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false);
expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false);
expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true);
expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true);
});
});
describe("canViewSubscription$", () => {
beforeEach(() => {
platformUtilsService.isSelfHost.mockReturnValue(false);
apiService.getUserBillingHistory.mockResolvedValue(
new BillingHistoryResponse({ invoices: [], transactions: [] }),
);
});
it("returns true when user has premium personally", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: true,
hasPremiumFromAnyOrganization: true,
});
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true);
});
it("returns true when user has no premium from any source", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: false,
});
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true);
});
it("returns true when user has billing history in cloud environment", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: true,
});
platformUtilsService.isSelfHost.mockReturnValue(false);
apiService.getUserBillingHistory.mockResolvedValue(
new BillingHistoryResponse({
invoices: [{ id: "1" }],
transactions: [{ id: "2" }],
}),
);
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true);
});
it("returns false when user has no premium personally, has org premium, and no billing history", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: true,
});
platformUtilsService.isSelfHost.mockReturnValue(false);
apiService.getUserBillingHistory.mockResolvedValue(
new BillingHistoryResponse({
invoices: [],
transactions: [],
}),
);
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false);
});
it("returns false when user has no premium personally, has org premium, in self-hosted environment", async () => {
userBillingAccountProfileState.nextState({
hasPremiumPersonally: false,
hasPremiumFromAnyOrganization: true,
});
platformUtilsService.isSelfHost.mockReturnValue(true);
expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false);
expect(apiService.getUserBillingHistory).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,11 +1,8 @@
import { map, Observable, of, switchMap } from "rxjs";
import { map, Observable, combineLatest, concatMap } from "rxjs";
import {
ActiveUserState,
BILLING_DISK,
StateProvider,
UserKeyDefinition,
} from "../../../platform/state";
import { ApiService } from "../../../abstractions/api.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { BILLING_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import {
BillingAccountProfile,
@@ -22,42 +19,34 @@ export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition<Bill
);
export class DefaultBillingAccountProfileStateService implements BillingAccountProfileStateService {
private billingAccountProfileState: ActiveUserState<BillingAccountProfile>;
constructor(
private readonly stateProvider: StateProvider,
private readonly platformUtilsService: PlatformUtilsService,
private readonly apiService: ApiService,
) {}
hasPremiumFromAnyOrganization$: Observable<boolean>;
hasPremiumPersonally$: Observable<boolean>;
hasPremiumFromAnySource$: Observable<boolean>;
hasPremiumFromAnyOrganization$(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION)
.state$.pipe(map((profile) => !!profile?.hasPremiumFromAnyOrganization));
}
constructor(private readonly stateProvider: StateProvider) {
this.billingAccountProfileState = stateProvider.getActive(
BILLING_ACCOUNT_PROFILE_KEY_DEFINITION,
);
hasPremiumPersonally$(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION)
.state$.pipe(map((profile) => !!profile?.hasPremiumPersonally));
}
// Setup an observable that will always track the currently active user
// but will fallback to emitting null when there is no active user.
const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe(
switchMap((userId) =>
userId != null
? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$
: of(null),
),
);
this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe(
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization),
);
this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe(
map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally),
);
this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe(
map(
(billingAccountProfile) =>
billingAccountProfile?.hasPremiumFromAnyOrganization === true ||
billingAccountProfile?.hasPremiumPersonally === true,
),
);
hasPremiumFromAnySource$(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION)
.state$.pipe(
map(
(profile) =>
profile?.hasPremiumFromAnyOrganization === true ||
profile?.hasPremiumPersonally === true,
),
);
}
async setHasPremium(
@@ -72,4 +61,23 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP
};
});
}
canViewSubscription$(userId: UserId): Observable<boolean> {
return combineLatest([
this.hasPremiumPersonally$(userId),
this.hasPremiumFromAnyOrganization$(userId),
]).pipe(
concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => {
const isCloud = !this.platformUtilsService.isSelfHost();
let billing = null;
if (isCloud) {
billing = await this.apiService.getUserBillingHistory();
}
const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory;
return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory;
}),
);
}
}

View File

@@ -1,24 +1,25 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response";
import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ToastService } from "@bitwarden/components";
import { ApiService } from "../../abstractions/api.service";
import { BillingApiServiceAbstraction } from "../../billing/abstractions";
import { PaymentMethodType } from "../../billing/enums";
import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request";
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
import { PlanResponse } from "../../billing/models/response/plan.response";
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
import { ErrorResponse } from "../../models/response/error.response";
import { ListResponse } from "../../models/response/list.response";
import { LogService } from "../../platform/abstractions/log.service";
import { BillingApiServiceAbstraction } from "../abstractions";
import { PaymentMethodType } from "../enums";
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request";
import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request";
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request";
import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request";
import { InvoicesResponse } from "../models/response/invoices.response";
import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response";
import { PaymentMethodResponse } from "../models/response/payment-method.response";
import { PlanResponse } from "../models/response/plan.response";
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
export class BillingApiService implements BillingApiServiceAbstraction {
@@ -214,6 +215,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
);
}
async restartSubscription(
organizationId: string,
request: OrganizationCreateRequest,
): Promise<void> {
return await this.apiService.send(
"POST",
"/organizations/" + organizationId + "/billing/restart-subscription",
request,
true,
false,
);
}
private async execute(request: () => Promise<any>): Promise<any> {
try {
return await request();

View File

@@ -1,18 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
BillingApiServiceAbstraction,
OrganizationBillingServiceAbstraction,
OrganizationInformation,
PaymentInformation,
PlanInformation,
SubscriptionInformation,
} from "@bitwarden/common/billing/abstractions";
import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response";
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
@@ -20,12 +7,25 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import { ConfigService } from "../../platform/abstractions/config/config.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { EncString } from "../../platform/models/domain/enc-string";
import { SyncService } from "../../platform/sync";
import { OrgKey } from "../../types/key";
import {
BillingApiServiceAbstraction,
OrganizationBillingServiceAbstraction,
OrganizationInformation,
PaymentInformation,
PlanInformation,
SubscriptionInformation,
} from "../abstractions";
import { PlanType } from "../enums";
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
import { BillingSourceResponse } from "../models/response/billing.response";
import { PaymentSourceResponse } from "../models/response/payment-source.response";
interface OrganizationKeys {
encryptedKey: EncString;
@@ -223,4 +223,17 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
request.additionalStorageGb = information.storage;
}
}
async restartSubscription(
organizationId: string,
subscription: SubscriptionInformation,
): Promise<void> {
const request = new OrganizationCreateRequest();
const organizationKeys = await this.makeOrganizationKeys();
this.setOrganizationKeys(request, organizationKeys);
this.setOrganizationInformation(request, subscription.organization);
this.setPlanInformation(request, subscription.plan);
this.setPaymentInformation(request, subscription.payment);
await this.billingApiService.restartSubscription(organizationId, request);
}
}

View File

@@ -0,0 +1,303 @@
import { ApiService } from "../../abstractions/api.service";
import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction";
import { CountryListItem } from "../models/domain";
import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request";
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
export class TaxService implements TaxServiceAbstraction {
constructor(private apiService: ApiService) {}
getCountries(): CountryListItem[] {
return [
{ name: "-- Select --", value: "", disabled: false },
{ name: "United States", value: "US", disabled: false },
{ name: "China", value: "CN", disabled: false },
{ name: "France", value: "FR", disabled: false },
{ name: "Germany", value: "DE", disabled: false },
{ name: "Canada", value: "CA", disabled: false },
{ name: "United Kingdom", value: "GB", disabled: false },
{ name: "Australia", value: "AU", disabled: false },
{ name: "India", value: "IN", disabled: false },
{ name: "", value: "-", disabled: true },
{ name: "Afghanistan", value: "AF", disabled: false },
{ name: "Åland Islands", value: "AX", disabled: false },
{ name: "Albania", value: "AL", disabled: false },
{ name: "Algeria", value: "DZ", disabled: false },
{ name: "American Samoa", value: "AS", disabled: false },
{ name: "Andorra", value: "AD", disabled: false },
{ name: "Angola", value: "AO", disabled: false },
{ name: "Anguilla", value: "AI", disabled: false },
{ name: "Antarctica", value: "AQ", disabled: false },
{ name: "Antigua and Barbuda", value: "AG", disabled: false },
{ name: "Argentina", value: "AR", disabled: false },
{ name: "Armenia", value: "AM", disabled: false },
{ name: "Aruba", value: "AW", disabled: false },
{ name: "Austria", value: "AT", disabled: false },
{ name: "Azerbaijan", value: "AZ", disabled: false },
{ name: "Bahamas", value: "BS", disabled: false },
{ name: "Bahrain", value: "BH", disabled: false },
{ name: "Bangladesh", value: "BD", disabled: false },
{ name: "Barbados", value: "BB", disabled: false },
{ name: "Belarus", value: "BY", disabled: false },
{ name: "Belgium", value: "BE", disabled: false },
{ name: "Belize", value: "BZ", disabled: false },
{ name: "Benin", value: "BJ", disabled: false },
{ name: "Bermuda", value: "BM", disabled: false },
{ name: "Bhutan", value: "BT", disabled: false },
{ name: "Bolivia, Plurinational State of", value: "BO", disabled: false },
{ name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false },
{ name: "Bosnia and Herzegovina", value: "BA", disabled: false },
{ name: "Botswana", value: "BW", disabled: false },
{ name: "Bouvet Island", value: "BV", disabled: false },
{ name: "Brazil", value: "BR", disabled: false },
{ name: "British Indian Ocean Territory", value: "IO", disabled: false },
{ name: "Brunei Darussalam", value: "BN", disabled: false },
{ name: "Bulgaria", value: "BG", disabled: false },
{ name: "Burkina Faso", value: "BF", disabled: false },
{ name: "Burundi", value: "BI", disabled: false },
{ name: "Cambodia", value: "KH", disabled: false },
{ name: "Cameroon", value: "CM", disabled: false },
{ name: "Cape Verde", value: "CV", disabled: false },
{ name: "Cayman Islands", value: "KY", disabled: false },
{ name: "Central African Republic", value: "CF", disabled: false },
{ name: "Chad", value: "TD", disabled: false },
{ name: "Chile", value: "CL", disabled: false },
{ name: "Christmas Island", value: "CX", disabled: false },
{ name: "Cocos (Keeling) Islands", value: "CC", disabled: false },
{ name: "Colombia", value: "CO", disabled: false },
{ name: "Comoros", value: "KM", disabled: false },
{ name: "Congo", value: "CG", disabled: false },
{ name: "Congo, the Democratic Republic of the", value: "CD", disabled: false },
{ name: "Cook Islands", value: "CK", disabled: false },
{ name: "Costa Rica", value: "CR", disabled: false },
{ name: "Côte d'Ivoire", value: "CI", disabled: false },
{ name: "Croatia", value: "HR", disabled: false },
{ name: "Cuba", value: "CU", disabled: false },
{ name: "Curaçao", value: "CW", disabled: false },
{ name: "Cyprus", value: "CY", disabled: false },
{ name: "Czech Republic", value: "CZ", disabled: false },
{ name: "Denmark", value: "DK", disabled: false },
{ name: "Djibouti", value: "DJ", disabled: false },
{ name: "Dominica", value: "DM", disabled: false },
{ name: "Dominican Republic", value: "DO", disabled: false },
{ name: "Ecuador", value: "EC", disabled: false },
{ name: "Egypt", value: "EG", disabled: false },
{ name: "El Salvador", value: "SV", disabled: false },
{ name: "Equatorial Guinea", value: "GQ", disabled: false },
{ name: "Eritrea", value: "ER", disabled: false },
{ name: "Estonia", value: "EE", disabled: false },
{ name: "Ethiopia", value: "ET", disabled: false },
{ name: "Falkland Islands (Malvinas)", value: "FK", disabled: false },
{ name: "Faroe Islands", value: "FO", disabled: false },
{ name: "Fiji", value: "FJ", disabled: false },
{ name: "Finland", value: "FI", disabled: false },
{ name: "French Guiana", value: "GF", disabled: false },
{ name: "French Polynesia", value: "PF", disabled: false },
{ name: "French Southern Territories", value: "TF", disabled: false },
{ name: "Gabon", value: "GA", disabled: false },
{ name: "Gambia", value: "GM", disabled: false },
{ name: "Georgia", value: "GE", disabled: false },
{ name: "Ghana", value: "GH", disabled: false },
{ name: "Gibraltar", value: "GI", disabled: false },
{ name: "Greece", value: "GR", disabled: false },
{ name: "Greenland", value: "GL", disabled: false },
{ name: "Grenada", value: "GD", disabled: false },
{ name: "Guadeloupe", value: "GP", disabled: false },
{ name: "Guam", value: "GU", disabled: false },
{ name: "Guatemala", value: "GT", disabled: false },
{ name: "Guernsey", value: "GG", disabled: false },
{ name: "Guinea", value: "GN", disabled: false },
{ name: "Guinea-Bissau", value: "GW", disabled: false },
{ name: "Guyana", value: "GY", disabled: false },
{ name: "Haiti", value: "HT", disabled: false },
{ name: "Heard Island and McDonald Islands", value: "HM", disabled: false },
{ name: "Holy See (Vatican City State)", value: "VA", disabled: false },
{ name: "Honduras", value: "HN", disabled: false },
{ name: "Hong Kong", value: "HK", disabled: false },
{ name: "Hungary", value: "HU", disabled: false },
{ name: "Iceland", value: "IS", disabled: false },
{ name: "Indonesia", value: "ID", disabled: false },
{ name: "Iran, Islamic Republic of", value: "IR", disabled: false },
{ name: "Iraq", value: "IQ", disabled: false },
{ name: "Ireland", value: "IE", disabled: false },
{ name: "Isle of Man", value: "IM", disabled: false },
{ name: "Israel", value: "IL", disabled: false },
{ name: "Italy", value: "IT", disabled: false },
{ name: "Jamaica", value: "JM", disabled: false },
{ name: "Japan", value: "JP", disabled: false },
{ name: "Jersey", value: "JE", disabled: false },
{ name: "Jordan", value: "JO", disabled: false },
{ name: "Kazakhstan", value: "KZ", disabled: false },
{ name: "Kenya", value: "KE", disabled: false },
{ name: "Kiribati", value: "KI", disabled: false },
{ name: "Korea, Democratic People's Republic of", value: "KP", disabled: false },
{ name: "Korea, Republic of", value: "KR", disabled: false },
{ name: "Kuwait", value: "KW", disabled: false },
{ name: "Kyrgyzstan", value: "KG", disabled: false },
{ name: "Lao People's Democratic Republic", value: "LA", disabled: false },
{ name: "Latvia", value: "LV", disabled: false },
{ name: "Lebanon", value: "LB", disabled: false },
{ name: "Lesotho", value: "LS", disabled: false },
{ name: "Liberia", value: "LR", disabled: false },
{ name: "Libya", value: "LY", disabled: false },
{ name: "Liechtenstein", value: "LI", disabled: false },
{ name: "Lithuania", value: "LT", disabled: false },
{ name: "Luxembourg", value: "LU", disabled: false },
{ name: "Macao", value: "MO", disabled: false },
{ name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false },
{ name: "Madagascar", value: "MG", disabled: false },
{ name: "Malawi", value: "MW", disabled: false },
{ name: "Malaysia", value: "MY", disabled: false },
{ name: "Maldives", value: "MV", disabled: false },
{ name: "Mali", value: "ML", disabled: false },
{ name: "Malta", value: "MT", disabled: false },
{ name: "Marshall Islands", value: "MH", disabled: false },
{ name: "Martinique", value: "MQ", disabled: false },
{ name: "Mauritania", value: "MR", disabled: false },
{ name: "Mauritius", value: "MU", disabled: false },
{ name: "Mayotte", value: "YT", disabled: false },
{ name: "Mexico", value: "MX", disabled: false },
{ name: "Micronesia, Federated States of", value: "FM", disabled: false },
{ name: "Moldova, Republic of", value: "MD", disabled: false },
{ name: "Monaco", value: "MC", disabled: false },
{ name: "Mongolia", value: "MN", disabled: false },
{ name: "Montenegro", value: "ME", disabled: false },
{ name: "Montserrat", value: "MS", disabled: false },
{ name: "Morocco", value: "MA", disabled: false },
{ name: "Mozambique", value: "MZ", disabled: false },
{ name: "Myanmar", value: "MM", disabled: false },
{ name: "Namibia", value: "NA", disabled: false },
{ name: "Nauru", value: "NR", disabled: false },
{ name: "Nepal", value: "NP", disabled: false },
{ name: "Netherlands", value: "NL", disabled: false },
{ name: "New Caledonia", value: "NC", disabled: false },
{ name: "New Zealand", value: "NZ", disabled: false },
{ name: "Nicaragua", value: "NI", disabled: false },
{ name: "Niger", value: "NE", disabled: false },
{ name: "Nigeria", value: "NG", disabled: false },
{ name: "Niue", value: "NU", disabled: false },
{ name: "Norfolk Island", value: "NF", disabled: false },
{ name: "Northern Mariana Islands", value: "MP", disabled: false },
{ name: "Norway", value: "NO", disabled: false },
{ name: "Oman", value: "OM", disabled: false },
{ name: "Pakistan", value: "PK", disabled: false },
{ name: "Palau", value: "PW", disabled: false },
{ name: "Palestinian Territory, Occupied", value: "PS", disabled: false },
{ name: "Panama", value: "PA", disabled: false },
{ name: "Papua New Guinea", value: "PG", disabled: false },
{ name: "Paraguay", value: "PY", disabled: false },
{ name: "Peru", value: "PE", disabled: false },
{ name: "Philippines", value: "PH", disabled: false },
{ name: "Pitcairn", value: "PN", disabled: false },
{ name: "Poland", value: "PL", disabled: false },
{ name: "Portugal", value: "PT", disabled: false },
{ name: "Puerto Rico", value: "PR", disabled: false },
{ name: "Qatar", value: "QA", disabled: false },
{ name: "Réunion", value: "RE", disabled: false },
{ name: "Romania", value: "RO", disabled: false },
{ name: "Russian Federation", value: "RU", disabled: false },
{ name: "Rwanda", value: "RW", disabled: false },
{ name: "Saint Barthélemy", value: "BL", disabled: false },
{ name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false },
{ name: "Saint Kitts and Nevis", value: "KN", disabled: false },
{ name: "Saint Lucia", value: "LC", disabled: false },
{ name: "Saint Martin (French part)", value: "MF", disabled: false },
{ name: "Saint Pierre and Miquelon", value: "PM", disabled: false },
{ name: "Saint Vincent and the Grenadines", value: "VC", disabled: false },
{ name: "Samoa", value: "WS", disabled: false },
{ name: "San Marino", value: "SM", disabled: false },
{ name: "Sao Tome and Principe", value: "ST", disabled: false },
{ name: "Saudi Arabia", value: "SA", disabled: false },
{ name: "Senegal", value: "SN", disabled: false },
{ name: "Serbia", value: "RS", disabled: false },
{ name: "Seychelles", value: "SC", disabled: false },
{ name: "Sierra Leone", value: "SL", disabled: false },
{ name: "Singapore", value: "SG", disabled: false },
{ name: "Sint Maarten (Dutch part)", value: "SX", disabled: false },
{ name: "Slovakia", value: "SK", disabled: false },
{ name: "Slovenia", value: "SI", disabled: false },
{ name: "Solomon Islands", value: "SB", disabled: false },
{ name: "Somalia", value: "SO", disabled: false },
{ name: "South Africa", value: "ZA", disabled: false },
{ name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false },
{ name: "South Sudan", value: "SS", disabled: false },
{ name: "Spain", value: "ES", disabled: false },
{ name: "Sri Lanka", value: "LK", disabled: false },
{ name: "Sudan", value: "SD", disabled: false },
{ name: "Suriname", value: "SR", disabled: false },
{ name: "Svalbard and Jan Mayen", value: "SJ", disabled: false },
{ name: "Swaziland", value: "SZ", disabled: false },
{ name: "Sweden", value: "SE", disabled: false },
{ name: "Switzerland", value: "CH", disabled: false },
{ name: "Syrian Arab Republic", value: "SY", disabled: false },
{ name: "Taiwan", value: "TW", disabled: false },
{ name: "Tajikistan", value: "TJ", disabled: false },
{ name: "Tanzania, United Republic of", value: "TZ", disabled: false },
{ name: "Thailand", value: "TH", disabled: false },
{ name: "Timor-Leste", value: "TL", disabled: false },
{ name: "Togo", value: "TG", disabled: false },
{ name: "Tokelau", value: "TK", disabled: false },
{ name: "Tonga", value: "TO", disabled: false },
{ name: "Trinidad and Tobago", value: "TT", disabled: false },
{ name: "Tunisia", value: "TN", disabled: false },
{ name: "Turkey", value: "TR", disabled: false },
{ name: "Turkmenistan", value: "TM", disabled: false },
{ name: "Turks and Caicos Islands", value: "TC", disabled: false },
{ name: "Tuvalu", value: "TV", disabled: false },
{ name: "Uganda", value: "UG", disabled: false },
{ name: "Ukraine", value: "UA", disabled: false },
{ name: "United Arab Emirates", value: "AE", disabled: false },
{ name: "United States Minor Outlying Islands", value: "UM", disabled: false },
{ name: "Uruguay", value: "UY", disabled: false },
{ name: "Uzbekistan", value: "UZ", disabled: false },
{ name: "Vanuatu", value: "VU", disabled: false },
{ name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false },
{ name: "Viet Nam", value: "VN", disabled: false },
{ name: "Virgin Islands, British", value: "VG", disabled: false },
{ name: "Virgin Islands, U.S.", value: "VI", disabled: false },
{ name: "Wallis and Futuna", value: "WF", disabled: false },
{ name: "Western Sahara", value: "EH", disabled: false },
{ name: "Yemen", value: "YE", disabled: false },
{ name: "Zambia", value: "ZM", disabled: false },
{ name: "Zimbabwe", value: "ZW", disabled: false },
];
}
async isCountrySupported(country: string): Promise<boolean> {
const response = await this.apiService.send(
"GET",
"/tax/is-country-supported?country=" + country,
null,
true,
true,
);
return response;
}
async previewIndividualInvoice(
request: PreviewIndividualInvoiceRequest,
): Promise<PreviewInvoiceResponse> {
const response = await this.apiService.send(
"POST",
"/accounts/billing/preview-invoice",
request,
true,
true,
);
return new PreviewInvoiceResponse(response);
}
async previewOrganizationInvoice(
request: PreviewOrganizationInvoiceRequest,
): Promise<PreviewInvoiceResponse> {
const response = await this.apiService.send(
"POST",
`/invoices/preview-organization`,
request,
true,
true,
);
return new PreviewInvoiceResponse(response);
}
}

View File

@@ -27,18 +27,40 @@ export enum DeviceType {
LinuxCLI = 25,
}
export const MobileDeviceTypes: Set<DeviceType> = new Set([
DeviceType.Android,
DeviceType.iOS,
DeviceType.AndroidAmazon,
]);
/**
* Device type metadata
* Each device type has a category corresponding to the client type and platform (Android, iOS, Chrome, Firefox, etc.)
*/
interface DeviceTypeMetadata {
category: "mobile" | "extension" | "webVault" | "desktop" | "cli" | "sdk" | "server";
platform: string;
}
export const DesktopDeviceTypes: Set<DeviceType> = new Set([
DeviceType.WindowsDesktop,
DeviceType.MacOsDesktop,
DeviceType.LinuxDesktop,
DeviceType.UWP,
DeviceType.WindowsCLI,
DeviceType.MacOsCLI,
DeviceType.LinuxCLI,
]);
export const DeviceTypeMetadata: Record<DeviceType, DeviceTypeMetadata> = {
[DeviceType.Android]: { category: "mobile", platform: "Android" },
[DeviceType.iOS]: { category: "mobile", platform: "iOS" },
[DeviceType.AndroidAmazon]: { category: "mobile", platform: "Amazon" },
[DeviceType.ChromeExtension]: { category: "extension", platform: "Chrome" },
[DeviceType.FirefoxExtension]: { category: "extension", platform: "Firefox" },
[DeviceType.OperaExtension]: { category: "extension", platform: "Opera" },
[DeviceType.EdgeExtension]: { category: "extension", platform: "Edge" },
[DeviceType.VivaldiExtension]: { category: "extension", platform: "Vivaldi" },
[DeviceType.SafariExtension]: { category: "extension", platform: "Safari" },
[DeviceType.ChromeBrowser]: { category: "webVault", platform: "Chrome" },
[DeviceType.FirefoxBrowser]: { category: "webVault", platform: "Firefox" },
[DeviceType.OperaBrowser]: { category: "webVault", platform: "Opera" },
[DeviceType.EdgeBrowser]: { category: "webVault", platform: "Edge" },
[DeviceType.IEBrowser]: { category: "webVault", platform: "IE" },
[DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" },
[DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" },
[DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" },
[DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" },
[DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" },
[DeviceType.LinuxDesktop]: { category: "desktop", platform: "Linux" },
[DeviceType.UWP]: { category: "desktop", platform: "Windows UWP" },
[DeviceType.WindowsCLI]: { category: "cli", platform: "Windows" },
[DeviceType.MacOsCLI]: { category: "cli", platform: "macOS" },
[DeviceType.LinuxCLI]: { category: "cli", platform: "Linux" },
[DeviceType.SDK]: { category: "sdk", platform: "" },
[DeviceType.Server]: { category: "server", platform: "" },
};

View File

@@ -4,29 +4,32 @@
* Flags MUST be short lived and SHALL be removed once enabled.
*/
export enum FeatureFlag {
BrowserFilelessImport = "browser-fileless-import",
/* Autofill */
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
IdpAutoSubmitLogin = "idp-auto-submit-login",
InlineMenuFieldQualification = "inline-menu-field-qualification",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
InlineMenuTotp = "inline-menu-totp",
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
NotificationRefresh = "notification-refresh",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
ItemShare = "item-share",
GeneratorToolsModernization = "generator-tools-modernization",
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
ExtensionRefresh = "extension-refresh",
PersistPopupView = "persist-popup-view",
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
EmailVerification = "email-verification",
InlineMenuFieldQualification = "inline-menu-field-qualification",
TwoFactorComponentRefactor = "two-factor-component-refactor",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
VaultBulkManagementAction = "vault-bulk-management-action",
IdpAutoSubmitLogin = "idp-auto-submit-login",
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
AccountDeprovisioning = "pm-10308-account-deprovisioning",
SSHKeyVaultItem = "ssh-key-vault-item",
SSHAgent = "ssh-agent",
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
CipherKeyEncryption = "cipher-key-encryption",
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
@@ -38,11 +41,9 @@ export enum FeatureFlag {
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
InlineMenuTotp = "inline-menu-totp",
MacOsNativeCredentialSync = "macos-native-credential-sync",
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
PM12443RemovePagingLogic = "pm-12443-remove-paging-logic",
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -57,29 +58,32 @@ const FALSE = false as boolean;
* We support true as a value as we prefer flags to "enable" not "disable".
*/
export const DefaultFeatureFlagValue = {
[FeatureFlag.BrowserFilelessImport]: FALSE,
/* Autofill */
[FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE,
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.InlineMenuTotp]: FALSE,
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
[FeatureFlag.NotificationRefresh]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.ItemShare]: FALSE,
[FeatureFlag.GeneratorToolsModernization]: FALSE,
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
[FeatureFlag.ExtensionRefresh]: FALSE,
[FeatureFlag.PersistPopupView]: FALSE,
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.EmailVerification]: FALSE,
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.VaultBulkManagementAction]: FALSE,
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
[FeatureFlag.AccountDeprovisioning]: FALSE,
[FeatureFlag.SSHKeyVaultItem]: FALSE,
[FeatureFlag.SSHAgent]: FALSE,
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
@@ -91,11 +95,9 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
[FeatureFlag.InlineMenuTotp]: FALSE,
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
[FeatureFlag.PM12443RemovePagingLogic]: FALSE,
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -23,4 +23,5 @@ export enum NotificationType {
SyncOrganizations = 17,
SyncOrganizationStatusChanged = 18,
SyncOrganizationCollectionSettingChanged = 19,
}

View File

@@ -2,15 +2,18 @@
// @ts-strict-ignore
import { firstValueFrom, map, timeout } from "rxjs";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BiometricStateService } from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountService } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { UserId } from "../../types/guid";
import { ProcessReloadServiceAbstraction } from "../abstractions/process-reload.service";
@@ -24,6 +27,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private biometricStateService: BiometricStateService,
private accountService: AccountService,
private logService: LogService,
) {}
async startProcessReload(authService: AuthService): Promise<void> {
@@ -35,6 +39,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
let status = await firstValueFrom(authService.authStatusFor$(userId as UserId));
status = await authService.getAuthStatus(userId);
if (status === AuthenticationStatus.Unlocked) {
this.logService.info(
"[Process Reload Service] User unlocked, preventing process reload",
);
return;
}
}
@@ -51,6 +58,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
if (userId != null) {
const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
if (ephemeralPin != null) {
this.logService.info(
"[Process Reload Service] Ephemeral pin active, preventing process reload",
);
return;
}
}
@@ -93,7 +103,12 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract
await this.reloadCallback();
}
return;
} else {
this.logService.info(
"[Process Reload Service] Desktop ipc fingerprint validated, preventing process reload",
);
}
if (this.reloadInterval == null) {
this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000);
}

View File

@@ -21,5 +21,5 @@ export const UriMatchStrategy = {
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
// using uniqueness properties of object shape over Set for ease of state storability
export type NeverDomains = { [id: string]: null };
export type NeverDomains = { [id: string]: null | { bannerIsDismissed?: boolean } };
export type EquivalentDomains = string[][];

View File

@@ -1,9 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SshKeyView as SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
import { EncString } from "../../platform/models/domain/enc-string";
import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key";
import { SshKeyView as SshKeyView } from "../../vault/models/view/ssh-key.view";
import { safeGetString } from "./utils";

View File

@@ -1,4 +1,4 @@
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { EncString } from "../../platform/models/domain/enc-string";
export function safeGetString(value: string | EncString) {
if (value == null) {

View File

@@ -45,6 +45,9 @@ export class NotificationResponse extends BaseResponse {
case NotificationType.SyncOrganizationStatusChanged:
this.payload = new OrganizationStatusPushNotification(payload);
break;
case NotificationType.SyncOrganizationCollectionSettingChanged:
this.payload = new OrganizationCollectionSettingChangedPushNotification(payload);
break;
default:
break;
}
@@ -126,3 +129,17 @@ export class OrganizationStatusPushNotification extends BaseResponse {
this.enabled = this.getResponseProperty("Enabled");
}
}
export class OrganizationCollectionSettingChangedPushNotification extends BaseResponse {
organizationId: string;
limitCollectionCreation: boolean;
limitCollectionDeletion: boolean;
constructor(response: any) {
super(response);
this.organizationId = this.getResponseProperty("OrganizationId");
this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation");
this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion");
}
}

View File

@@ -1,5 +1,5 @@
import { CsprngArray } from "../../types/csprng";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class CryptoFunctionService {
@@ -51,11 +51,13 @@ export abstract class CryptoFunctionService {
iv: string,
mac: string,
key: SymmetricCryptoKey,
): DecryptParameters<Uint8Array | string>;
abstract aesDecryptFast(
parameters: DecryptParameters<Uint8Array | string>,
mode: "cbc" | "ecb",
): Promise<string>;
): CbcDecryptParameters<Uint8Array | string>;
abstract aesDecryptFast({
mode,
parameters,
}:
| { mode: "cbc"; parameters: CbcDecryptParameters<Uint8Array | string> }
| { mode: "ecb"; parameters: EcbDecryptParameters<Uint8Array | string> }): Promise<string>;
abstract aesDecrypt(
data: Uint8Array,
iv: Uint8Array,

View File

@@ -8,12 +8,32 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export abstract class EncryptService {
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* Decrypts an EncString to a string
* @param encString - The EncString to decrypt
* @param key - The key to decrypt the EncString with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted string
*/
abstract decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptContext?: string,
decryptTrace?: string,
): Promise<string>;
abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array>;
/**
* Decrypts an Encrypted object to a Uint8Array
* @param encThing - The Encrypted object to decrypt
* @param key - The key to decrypt the Encrypted object with
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
* @returns The decrypted Uint8Array
*/
abstract decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptTrace?: string,
): Promise<Uint8Array>;
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;

View File

@@ -1,5 +1,4 @@
export enum KeySuffixOptions {
Auto = "auto",
Biometric = "biometric",
Pin = "pin",
}

View File

@@ -1,18 +1,17 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// required to avoid linting errors when there are no flags
// eslint-disable-next-line @typescript-eslint/ban-types
export type SharedFlags = {
sdk?: boolean;
prereleaseBuild?: boolean;
};
// required to avoid linting errors when there are no flags
// eslint-disable-next-line @typescript-eslint/ban-types
export type SharedDevFlags = {
noopNotifications: boolean;
skipWelcomeOnInstall: boolean;
configRetrievalIntervalMs: number;
showRiskInsightsDebug: boolean;
};
function getFlags<T>(envFlags: string | T): T {

View File

@@ -8,10 +8,14 @@ import { Observable, of, switchMap } from "rxjs";
import { getHostname, parse } from "tldts";
import { Merge } from "type-fest";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { I18nService } from "../abstractions/i18n.service";
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-require-imports
const nodeURL = typeof self === "undefined" ? require("url") : null;
declare global {
@@ -608,6 +612,8 @@ export class Utils {
}
return new URL(uriString);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
// Ignore error
}

View File

@@ -1,10 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export class DecryptParameters<T> {
export type CbcDecryptParameters<T> = {
encKey: T;
data: T;
iv: T;
macKey: T;
mac: T;
macKey?: T;
mac?: T;
macData: T;
}
};
export type EcbDecryptParameters<T> = {
encKey: T;
data: T;
};

View File

@@ -67,9 +67,13 @@ describe("DomainBase", () => {
);
// @ts-expect-error -- encString2 was not decrypted
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
decrypted as { encToString: string; encString2: string; plainText: string };
// encString2 was not decrypted, so it's still an EncString
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
decrypted as { encToString: string; encString2: EncString; plainText: string };
});

View File

@@ -8,7 +8,7 @@ import { EncryptService } from "../../abstractions/encrypt.service";
import { EncString } from "./enc-string";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
// eslint-disable-next-line @typescript-eslint/ban-types
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
type EncStringKeys<T> = ConditionalKeys<ConditionalExcept<T, Function>, EncString>;
export type DecryptedObject<
TEncryptedObject,
@@ -63,6 +63,7 @@ export default class Domain {
map: any,
orgId: string,
key: SymmetricCryptoKey = null,
objectContext: string = "No Domain Context",
): Promise<T> {
const promises = [];
const self: any = this;
@@ -78,7 +79,11 @@ export default class Domain {
.then(() => {
const mapProp = map[theProp] || theProp;
if (self[mapProp]) {
return self[mapProp].decrypt(orgId, key);
return self[mapProp].decrypt(
orgId,
key,
`Property: ${prop}; ObjectContext: ${objectContext}`,
);
}
return null;
})
@@ -114,12 +119,21 @@ export default class Domain {
key: SymmetricCryptoKey,
encryptService: EncryptService,
_: Constructor<TThis> = this.constructor as Constructor<TThis>,
objectContext: string = "No Domain Context",
): Promise<DecryptedObject<TThis, TEncryptedKeys>> {
const promises = [];
for (const prop of encryptedProperties) {
const value = (this as any)[prop] as EncString;
promises.push(this.decryptProperty(prop, value, key, encryptService));
promises.push(
this.decryptProperty(
prop,
value,
key,
encryptService,
`Property: ${prop.toString()}; ObjectContext: ${objectContext}`,
),
);
}
const decryptedObjects = await Promise.all(promises);
@@ -137,10 +151,11 @@ export default class Domain {
value: EncString,
key: SymmetricCryptoKey,
encryptService: EncryptService,
decryptTrace: string,
) {
let decrypted: string = null;
if (value) {
decrypted = await value.decryptWithKey(key, encryptService);
decrypted = await value.decryptWithKey(key, encryptService, decryptTrace);
} else {
decrypted = null;
}

View File

@@ -1,5 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
import { makeEncString, makeStaticByteArray } from "../../../../spec";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";

View File

@@ -125,6 +125,8 @@ export class EncString implements Encrypted {
try {
encType = parseInt(headerPieces[0], null);
encPieces = headerPieces[1].split("|");
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return { encType: NaN, encPieces: [] };
}
@@ -156,21 +158,21 @@ export class EncString implements Encrypted {
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
}
async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise<string> {
async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise<string> {
if (this.decryptedValue != null) {
return this.decryptedValue;
}
let keyContext = "provided-key";
let decryptTrace = "provided-key";
try {
if (key == null) {
key = await this.getKeyForDecryption(orgId);
keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
if (orgId != null) {
keyContext = `domain-orgkey-${orgId}`;
decryptTrace = `domain-orgkey-${orgId}`;
} else {
const cryptoService = Utils.getContainerService().getKeyService();
keyContext =
decryptTrace =
(await cryptoService.getUserKey()) == null
? "domain-withlegacysupport-masterkey"
: "domain-withlegacysupport-userkey";
@@ -181,20 +183,32 @@ export class EncString implements Encrypted {
}
const encryptService = Utils.getContainerService().getEncryptService();
this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext);
this.decryptedValue = await encryptService.decryptToUtf8(
this,
key,
decryptTrace == null ? context : `${decryptTrace}${context || ""}`,
);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.decryptedValue = DECRYPT_ERROR;
}
return this.decryptedValue;
}
async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) {
async decryptWithKey(
key: SymmetricCryptoKey,
encryptService: EncryptService,
decryptTrace: string = "domain-withkey",
): Promise<string> {
try {
if (key == null) {
throw new Error("No key to decrypt EncString");
}
this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey");
this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.decryptedValue = DECRYPT_ERROR;
}

View File

@@ -7,7 +7,7 @@ import { EncryptionType } from "../../enums";
export class SymmetricCryptoKey {
key: Uint8Array;
encKey?: Uint8Array;
encKey: Uint8Array;
macKey?: Uint8Array;
encType: EncryptionType;
@@ -48,12 +48,8 @@ export class SymmetricCryptoKey {
throw new Error("Unsupported encType/key length.");
}
if (this.key != null) {
this.keyB64 = Utils.fromBufferToB64(this.key);
}
if (this.encKey != null) {
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
}
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
if (this.macKey != null) {
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
}

View File

@@ -1,3 +1,5 @@
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { EncryptService } from "../abstractions/encrypt.service";

View File

@@ -114,7 +114,7 @@ export class EncryptServiceImplementation implements EncryptService {
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
this.logMacFailed(
"[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " +
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
encryptionTypeName(key.encType) +
"Payload type " +
encryptionTypeName(encString.encryptionType) +
@@ -125,10 +125,14 @@ export class EncryptServiceImplementation implements EncryptService {
}
}
return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc");
return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams });
}
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
async decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
): Promise<Uint8Array> {
if (key == null) {
throw new Error("No encryption key provided.");
}
@@ -145,7 +149,9 @@ export class EncryptServiceImplementation implements EncryptService {
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
@@ -155,7 +161,9 @@ export class EncryptServiceImplementation implements EncryptService {
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
@@ -167,11 +175,13 @@ export class EncryptServiceImplementation implements EncryptService {
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
if (computedMac === null) {
this.logMacFailed(
"[Encrypt service] Failed to compute MAC." +
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}
@@ -179,11 +189,13 @@ export class EncryptServiceImplementation implements EncryptService {
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"[Encrypt service] MAC comparison failed. Key or payload has changed." +
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
" Key type " +
encryptionTypeName(key.encType) +
" Payload type " +
encryptionTypeName(encThing.encryptionType),
encryptionTypeName(encThing.encryptionType) +
" Decrypt context: " +
decryptContext,
);
return null;
}

View File

@@ -1,14 +1,18 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, Observable } from "rxjs";
// FIXME: remove `/apps` import from `/libs`
// eslint-disable-next-line import/no-restricted-paths
import { flushPromises } from "@bitwarden/browser/src/autofill/spec/testing-utils";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { Fido2ActiveRequestManager } from "./fido2-active-request-manager";
// Duplicated from `apps/browser/src/autofill/spec/testing-utils.ts`.
const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout;
function flushPromises() {
return new Promise(function (resolve) {
scheduler(resolve);
});
}
jest.mock("rxjs", () => {
const rxjs = jest.requireActual("rxjs");
const { firstValueFrom } = rxjs;

View File

@@ -1,5 +1,7 @@
import { mock } from "jest-mock-extended";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { DefaultKeyService } from "../../../../key-management/src/key.service";
import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid";

View File

@@ -1,3 +1,5 @@
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { UserId } from "../../types/guid";
import { KeySuffixOptions } from "../enums";

View File

@@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended";
import { Utils } from "../../platform/misc/utils";
import { PlatformUtilsService } from "../abstractions/platform-utils.service";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { EcbDecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { WebCryptoFunctionService } from "./web-crypto-function.service";
@@ -253,8 +253,13 @@ describe("WebCrypto Function Service", () => {
const encData = Utils.fromBufferToB64(encValue);
const b64Iv = Utils.fromBufferToB64(iv);
const symKey = new SymmetricCryptoKey(key);
const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc");
const parameters = cryptoFunctionService.aesDecryptFastParameters(
encData,
b64Iv,
null,
symKey,
);
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
expect(decValue).toBe(value);
});
@@ -276,8 +281,8 @@ describe("WebCrypto Function Service", () => {
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, "cbc");
const parameters = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
expect(decValue).toBe("EncryptMe!");
});
});
@@ -287,10 +292,11 @@ describe("WebCrypto Function Service", () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw==");
const params = new DecryptParameters<string>();
params.encKey = Utils.fromBufferToByteString(key);
params.data = Utils.fromBufferToByteString(data);
const decValue = await cryptoFunctionService.aesDecryptFast(params, "ecb");
const parameters: EcbDecryptParameters<string> = {
encKey: Utils.fromBufferToByteString(key),
data: Utils.fromBufferToByteString(data),
};
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "ecb", parameters });
expect(decValue).toBe("EncryptMe!");
});
});
@@ -304,6 +310,15 @@ describe("WebCrypto Function Service", () => {
const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "cbc");
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
});
it("throws if iv is not provided", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
await expect(() => cryptoFunctionService.aesDecrypt(data, null, key, "cbc")).rejects.toThrow(
"IV is required for CBC mode",
);
});
});
describe("aesDecrypt ECB mode", () => {

View File

@@ -1,12 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as argon2 from "argon2-browser";
import * as forge from "node-forge";
import { Utils } from "../../platform/misc/utils";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
export class WebCryptoFunctionService implements CryptoFunctionService {
@@ -14,10 +12,14 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
private subtle: SubtleCrypto;
private wasmSupported: boolean;
constructor(globalContext: Window | typeof global) {
this.crypto = typeof globalContext.crypto !== "undefined" ? globalContext.crypto : null;
this.subtle =
!!this.crypto && typeof this.crypto.subtle !== "undefined" ? this.crypto.subtle : null;
constructor(globalContext: { crypto: Crypto }) {
if (globalContext?.crypto?.subtle == null) {
throw new Error(
"Could not instantiate WebCryptoFunctionService. Could not locate Subtle crypto.",
);
}
this.crypto = globalContext.crypto;
this.subtle = this.crypto.subtle;
this.wasmSupported = this.checkIfWasmSupported();
}
@@ -220,7 +222,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
hmac.update(a);
const mac1 = hmac.digest().getBytes();
hmac.start(null, null);
hmac.start("sha256", null);
hmac.update(b);
const mac2 = hmac.digest().getBytes();
@@ -239,10 +241,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
aesDecryptFastParameters(
data: string,
iv: string,
mac: string,
mac: string | null,
key: SymmetricCryptoKey,
): DecryptParameters<string> {
const p = new DecryptParameters<string>();
): CbcDecryptParameters<string> {
const p = {} as CbcDecryptParameters<string>;
if (key.meta != null) {
p.encKey = key.meta.encKeyByteString;
p.macKey = key.meta.macKeyByteString;
@@ -275,7 +277,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return p;
}
aesDecryptFast(parameters: DecryptParameters<string>, mode: "cbc" | "ecb"): Promise<string> {
aesDecryptFast({
mode,
parameters,
}:
| { mode: "cbc"; parameters: CbcDecryptParameters<string> }
| { mode: "ecb"; parameters: EcbDecryptParameters<string> }): Promise<string> {
const decipher = (forge as any).cipher.createDecipher(
this.toWebCryptoAesMode(mode),
parameters.encKey,
@@ -294,21 +301,27 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
async aesDecrypt(
data: Uint8Array,
iv: Uint8Array,
iv: Uint8Array | null,
key: Uint8Array,
mode: "cbc" | "ecb",
): Promise<Uint8Array> {
if (mode === "ecb") {
// Web crypto does not support AES-ECB mode, so we need to do this in forge.
const params = new DecryptParameters<string>();
params.data = this.toByteString(data);
params.encKey = this.toByteString(key);
const result = await this.aesDecryptFast(params, "ecb");
const parameters: EcbDecryptParameters<string> = {
data: this.toByteString(data),
encKey: this.toByteString(key),
};
const result = await this.aesDecryptFast({ mode: "ecb", parameters });
return Utils.fromByteStringToArray(result);
}
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"decrypt",
]);
// CBC
if (iv == null) {
throw new Error("IV is required for CBC mode.");
}
const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data);
return new Uint8Array(buffer);
}

View File

@@ -8,7 +8,14 @@ import { DerivedStateProvider } from "../derived-state.provider";
import { DefaultDerivedState } from "./default-derived-state";
export class DefaultDerivedStateProvider implements DerivedStateProvider {
private cache: Record<string, DerivedState<unknown>> = {};
/**
* The cache uses a WeakMap to maintain separate derived states per user.
* Each user's state Observable acts as a unique key, without needing to
* pass around `userId`. Also, when a user's state Observable is cleaned up
* (like during an account swap) their cache is automatically garbage
* collected.
*/
private cache = new WeakMap<Observable<unknown>, Record<string, DerivedState<unknown>>>();
constructor() {}
@@ -17,8 +24,14 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider {
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: TDeps,
): DerivedState<TTo> {
let stateCache = this.cache.get(parentState$);
if (!stateCache) {
stateCache = {};
this.cache.set(parentState$, stateCache);
}
const cacheKey = deriveDefinition.buildCacheKey();
const existingDerivedState = this.cache[cacheKey];
const existingDerivedState = stateCache[cacheKey];
if (existingDerivedState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
@@ -26,7 +39,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider {
}
const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies);
this.cache[cacheKey] = newDerivedState;
stateCache[cacheKey] = newDerivedState;
return newDerivedState;
}

View File

@@ -9,6 +9,7 @@ import { DeriveDefinition } from "../derive-definition";
import { StateDefinition } from "../state-definition";
import { DefaultDerivedState } from "./default-derived-state";
import { DefaultDerivedStateProvider } from "./default-derived-state.provider";
let callCount = 0;
const cleanupDelayMs = 10;
@@ -182,4 +183,29 @@ describe("DefaultDerivedState", () => {
expect(await firstValueFrom(observable)).toEqual(new Date(newDate));
});
});
describe("account switching", () => {
let provider: DefaultDerivedStateProvider;
beforeEach(() => {
provider = new DefaultDerivedStateProvider();
});
it("should provide a dedicated cache for each account", async () => {
const user1State$ = new Subject<string>();
const user1Derived = provider.get(user1State$, deriveDefinition, deps);
const user1Emissions = trackEmissions(user1Derived.state$);
const user2State$ = new Subject<string>();
const user2Derived = provider.get(user2State$, deriveDefinition, deps);
const user2Emissions = trackEmissions(user2Derived.state$);
user1State$.next("2015-12-30");
user2State$.next("2020-12-29");
await awaitAsync();
expect(user1Emissions).toEqual([new Date("2015-12-30")]);
expect(user2Emissions).toEqual([new Date("2020-12-29")]);
});
});
});

View File

@@ -150,6 +150,9 @@ export const COLLECTION_DATA = new StateDefinition("collection", "disk", {
web: "memory",
});
export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" });
export const FOLDER_MEMORY = new StateDefinition("decryptedFolders", "memory", {
browser: "memory-large-object",
});
export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", {
web: "disk-local",
});
@@ -176,9 +179,12 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro
web: "disk-local",
});
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk");
export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
"newDeviceVerificationNotice",
"disk",
{
web: "disk-local",
},
);
export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk");
export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");

View File

@@ -85,18 +85,25 @@ export abstract class CoreSyncService implements SyncService {
await this.stateProvider.getUser(userId, LAST_SYNC_DATE).update(() => date);
}
async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise<boolean> {
async syncUpsertFolder(
notification: SyncFolderNotification,
isEdit: boolean,
userId: UserId,
): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
if (authStatus >= AuthenticationStatus.Locked) {
try {
const localFolder = await this.folderService.get(notification.id);
const localFolder = await this.folderService.get(notification.id, userId);
if (
(!isEdit && localFolder == null) ||
(isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate)
) {
const remoteFolder = await this.folderApiService.get(notification.id);
if (remoteFolder != null) {
await this.folderService.upsert(new FolderData(remoteFolder));
await this.folderService.upsert(new FolderData(remoteFolder), userId);
this.messageSender.send("syncedUpsertedFolder", { folderId: notification.id });
return this.syncCompleted(true);
}
@@ -108,10 +115,13 @@ export abstract class CoreSyncService implements SyncService {
return this.syncCompleted(false);
}
async syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean> {
async syncDeleteFolder(notification: SyncFolderNotification, userId: UserId): Promise<boolean> {
this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
await this.folderService.delete(notification.id);
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
if (authStatus >= AuthenticationStatus.Locked) {
await this.folderService.delete(notification.id, userId);
this.messageSender.send("syncedDeletedFolder", { folderId: notification.id });
this.syncCompleted(true);
return true;

View File

@@ -8,8 +8,14 @@ import {
CollectionDetailsResponse,
} from "@bitwarden/admin-console/common";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { LogoutReason } from "../../../../auth/src/common/types";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";

View File

@@ -56,8 +56,9 @@ export abstract class SyncService {
abstract syncUpsertFolder(
notification: SyncFolderNotification,
isEdit: boolean,
userId: UserId,
): Promise<boolean>;
abstract syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean>;
abstract syncDeleteFolder(notification: SyncFolderNotification, userId: UserId): Promise<boolean>;
abstract syncUpsertCipher(
notification: SyncCipherNotification,
isEdit: boolean,

View File

@@ -3,9 +3,9 @@
import { firstValueFrom } from "rxjs";
import {
CollectionRequest,
CollectionAccessDetailsResponse,
CollectionDetailsResponse,
CollectionRequest,
CollectionResponse,
} from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
@@ -103,7 +103,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
import { PlanResponse } from "../billing/models/response/plan.response";
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
import { TaxRateResponse } from "../billing/models/response/tax-rate.response";
import { DeviceType } from "../enums";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
@@ -897,11 +896,6 @@ export class ApiService implements ApiServiceAbstraction {
return new ListResponse(r, PlanResponse);
}
async getTaxRates(): Promise<ListResponse<TaxRateResponse>> {
const r = await this.send("GET", "/plans/sales-tax-rates/", null, true, true);
return new ListResponse(r, TaxRateResponse);
}
// Settings APIs
async getSettingsDomains(): Promise<DomainsResponse> {
@@ -1835,7 +1829,7 @@ export class ApiService implements ApiServiceAbstraction {
}
async send(
method: "GET" | "POST" | "PUT" | "DELETE",
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: string,
body: any,
authed: boolean,
@@ -1875,7 +1869,7 @@ export class ApiService implements ApiServiceAbstraction {
return responseJson;
} else if (hasResponse && response.status === 200 && responseIsCsv) {
return await response.text();
} else if (response.status !== 200) {
} else if (response.status !== 200 && response.status !== 204) {
const error = await this.handleError(response, false, authed);
return Promise.reject(error);
}

View File

@@ -168,10 +168,14 @@ export class NotificationsService implements NotificationsServiceAbstraction {
await this.syncService.syncUpsertFolder(
notification.payload as SyncFolderNotification,
notification.type === NotificationType.SyncFolderUpdate,
payloadUserId,
);
break;
case NotificationType.SyncFolderDelete:
await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification);
await this.syncService.syncDeleteFolder(
notification.payload as SyncFolderNotification,
payloadUserId,
);
break;
case NotificationType.SyncVault:
case NotificationType.SyncCiphers:
@@ -223,6 +227,11 @@ export class NotificationsService implements NotificationsServiceAbstraction {
await this.syncService.fullSync(true);
}
break;
case NotificationType.SyncOrganizationCollectionSettingChanged:
if (isAuthenticated) {
await this.syncService.fullSync(true);
}
break;
default:
break;
}

View File

@@ -6,10 +6,10 @@ import {
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { BiometricStateService } from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
@@ -18,10 +18,12 @@ import { Policy } from "../../admin-console/models/domain/policy";
import { TokenService } from "../../auth/abstractions/token.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { LogService } from "../../platform/abstractions/log.service";
import { Utils } from "../../platform/misc/utils";
import {
VAULT_TIMEOUT,
VAULT_TIMEOUT_ACTION,
} from "../../services/vault-timeout/vault-timeout-settings.state";
import { UserId } from "../../types/guid";
import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type";
import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service";

View File

@@ -21,6 +21,8 @@ import {
} from "@bitwarden/auth/common";
import { BiometricStateService } from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";

View File

@@ -3,8 +3,7 @@ import { BehaviorSubject, from, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
import { BiometricsService } from "@bitwarden/key-management";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { SearchService } from "../../abstractions/search.service";
@@ -14,10 +13,12 @@ import { AuthService } from "../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { Utils } from "../../platform/misc/utils";
import { TaskSchedulerService } from "../../platform/scheduling";
import { StateEventRunnerService } from "../../platform/state";
import { UserId } from "../../types/guid";
import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type";
@@ -41,6 +42,7 @@ describe("VaultTimeoutService", () => {
let stateEventRunnerService: MockProxy<StateEventRunnerService>;
let taskSchedulerService: MockProxy<TaskSchedulerService>;
let logService: MockProxy<LogService>;
let biometricsService: MockProxy<BiometricsService>;
let lockedCallback: jest.Mock<Promise<void>, [userId: string]>;
let loggedOutCallback: jest.Mock<Promise<void>, [logoutReason: LogoutReason, userId?: string]>;
@@ -66,6 +68,7 @@ describe("VaultTimeoutService", () => {
stateEventRunnerService = mock();
taskSchedulerService = mock<TaskSchedulerService>();
logService = mock<LogService>();
biometricsService = mock<BiometricsService>();
lockedCallback = jest.fn();
loggedOutCallback = jest.fn();
@@ -93,6 +96,7 @@ describe("VaultTimeoutService", () => {
stateEventRunnerService,
taskSchedulerService,
logService,
biometricsService,
lockedCallback,
loggedOutCallback,
);
@@ -334,7 +338,7 @@ describe("VaultTimeoutService", () => {
// Active users should have additional steps ran
expect(searchService.clearIndex).toHaveBeenCalled();
expect(folderService.clearCache).toHaveBeenCalled();
expect(folderService.clearDecryptedFolderState).toHaveBeenCalled();
expectUserToHaveLoggedOut("3"); // They have chosen logout as their action and it's available, log them out
expectUserToHaveLoggedOut("4"); // They may have had lock as their chosen action but it's not available to them so logout

View File

@@ -4,8 +4,7 @@ import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from "
import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling";
import { BiometricsService } from "@bitwarden/key-management";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
@@ -15,9 +14,11 @@ import { AuthService } from "../../auth/abstractions/auth.service";
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { LogService } from "../../platform/abstractions/log.service";
import { MessagingService } from "../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { StateService } from "../../platform/abstractions/state.service";
import { TaskSchedulerService, ScheduledTaskNames } from "../../platform/scheduling";
import { StateEventRunnerService } from "../../platform/state";
import { UserId } from "../../types/guid";
import { CipherService } from "../../vault/abstractions/cipher.service";
@@ -41,6 +42,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private stateEventRunnerService: StateEventRunnerService,
private taskSchedulerService: TaskSchedulerService,
protected logService: LogService,
private biometricService: BiometricsService,
private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (
logoutReason: LogoutReason,
@@ -98,6 +100,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
}
async lock(userId?: UserId): Promise<void> {
await this.biometricService.setShouldAutopromptNow(false);
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
@@ -135,10 +139,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
if (userId == null || userId === currentUserId) {
await this.searchService.clearIndex();
await this.folderService.clearCache();
await this.collectionService.clearActiveUserCache();
}
await this.folderService.clearDecryptedFolderState(lockingUserId);
await this.masterPasswordService.clearMasterKey(lockingUserId);
await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId });

View File

@@ -3,6 +3,8 @@
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const AutofillOverlayVisibility = {
Off: 0,
OnButtonClick: 1,

Some files were not shown because too many files have changed in this diff Show More