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

Merge remote-tracking branch 'origin/main' into ac/strong-typed-guids

This commit is contained in:
Thomas Rittson
2025-07-25 14:22:44 +10:00
588 changed files with 20565 additions and 9808 deletions

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {
@@ -128,7 +126,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher
* of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service.
*/
export abstract class ApiService {
send: (
abstract send(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: string,
body: any,
@@ -136,196 +134,225 @@ export abstract class ApiService {
hasResponse: boolean,
apiUrl?: string | null,
alterHeaders?: (headers: Headers) => void,
) => Promise<any>;
): Promise<any>;
postIdentityToken: (
abstract postIdentityToken(
request:
| PasswordTokenRequest
| SsoTokenRequest
| UserApiTokenRequest
| WebAuthnLoginTokenRequest,
) => Promise<
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
>;
refreshIdentityToken: () => Promise<any>;
abstract refreshIdentityToken(): Promise<any>;
getProfile: () => Promise<ProfileResponse>;
getUserSubscription: () => Promise<SubscriptionResponse>;
getTaxInfo: () => Promise<TaxInfoResponse>;
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
postEmail: (request: EmailRequest) => Promise<any>;
postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise<any>;
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
getAccountRevisionDate: () => Promise<number>;
postPasswordHint: (request: PasswordHintRequest) => Promise<any>;
postPremium: (data: FormData) => Promise<PaymentResponse>;
postReinstatePremium: () => Promise<any>;
postAccountStorage: (request: StorageRequest) => Promise<PaymentResponse>;
postAccountPayment: (request: PaymentRequest) => Promise<void>;
postAccountLicense: (data: FormData) => Promise<any>;
postAccountKeys: (request: KeysRequest) => Promise<any>;
postAccountVerifyEmail: () => Promise<any>;
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
postAccountKdf: (request: KdfRequest) => Promise<any>;
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
postConvertToKeyConnector: () => Promise<void>;
abstract getProfile(): Promise<ProfileResponse>;
abstract getUserSubscription(): Promise<SubscriptionResponse>;
abstract getTaxInfo(): Promise<TaxInfoResponse>;
abstract putProfile(request: UpdateProfileRequest): Promise<ProfileResponse>;
abstract putAvatar(request: UpdateAvatarRequest): Promise<ProfileResponse>;
abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise<any>;
abstract postPrelogin(request: PreloginRequest): Promise<PreloginResponse>;
abstract postEmailToken(request: EmailTokenRequest): Promise<any>;
abstract postEmail(request: EmailRequest): Promise<any>;
abstract postSetKeyConnectorKey(request: SetKeyConnectorKeyRequest): Promise<any>;
abstract postSecurityStamp(request: SecretVerificationRequest): Promise<any>;
abstract getAccountRevisionDate(): Promise<number>;
abstract postPasswordHint(request: PasswordHintRequest): Promise<any>;
abstract postPremium(data: FormData): Promise<PaymentResponse>;
abstract postReinstatePremium(): Promise<any>;
abstract postAccountStorage(request: StorageRequest): Promise<PaymentResponse>;
abstract postAccountPayment(request: PaymentRequest): Promise<void>;
abstract postAccountLicense(data: FormData): Promise<any>;
abstract postAccountKeys(request: KeysRequest): Promise<any>;
abstract postAccountVerifyEmail(): Promise<any>;
abstract postAccountVerifyEmailToken(request: VerifyEmailRequest): Promise<any>;
abstract postAccountRecoverDelete(request: DeleteRecoverRequest): Promise<any>;
abstract postAccountRecoverDeleteToken(request: VerifyDeleteRecoverRequest): Promise<any>;
abstract postAccountKdf(request: KdfRequest): Promise<any>;
abstract postUserApiKey(id: string, request: SecretVerificationRequest): Promise<ApiKeyResponse>;
abstract postUserRotateApiKey(
id: string,
request: SecretVerificationRequest,
): Promise<ApiKeyResponse>;
abstract postConvertToKeyConnector(): Promise<void>;
//passwordless
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
getAuthRequests: () => Promise<ListResponse<AuthRequestResponse>>;
getLastAuthRequest: () => Promise<AuthRequestResponse>;
abstract getAuthRequest(id: string): Promise<AuthRequestResponse>;
abstract putAuthRequest(
id: string,
request: PasswordlessAuthRequest,
): Promise<AuthRequestResponse>;
abstract getAuthRequests(): Promise<ListResponse<AuthRequestResponse>>;
abstract getLastAuthRequest(): Promise<AuthRequestResponse>;
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
abstract getUserBillingHistory(): Promise<BillingHistoryResponse>;
abstract getUserBillingPayment(): Promise<BillingPaymentResponse>;
getCipher: (id: string) => Promise<CipherResponse>;
getFullCipherDetails: (id: string) => Promise<CipherResponse>;
getCipherAdmin: (id: string) => Promise<CipherResponse>;
getAttachmentData: (
abstract getCipher(id: string): Promise<CipherResponse>;
abstract getFullCipherDetails(id: string): Promise<CipherResponse>;
abstract getCipherAdmin(id: string): Promise<CipherResponse>;
abstract getAttachmentData(
cipherId: string,
attachmentId: string,
emergencyAccessId?: string,
) => Promise<AttachmentResponse>;
getAttachmentDataAdmin: (cipherId: string, attachmentId: string) => Promise<AttachmentResponse>;
getCiphersOrganization: (organizationId: string) => Promise<ListResponse<CipherResponse>>;
postCipher: (request: CipherRequest) => Promise<CipherResponse>;
postCipherCreate: (request: CipherCreateRequest) => Promise<CipherResponse>;
postCipherAdmin: (request: CipherCreateRequest) => Promise<CipherResponse>;
putCipher: (id: string, request: CipherRequest) => Promise<CipherResponse>;
putPartialCipher: (id: string, request: CipherPartialRequest) => Promise<CipherResponse>;
putCipherAdmin: (id: string, request: CipherRequest) => Promise<CipherResponse>;
deleteCipher: (id: string) => Promise<any>;
deleteCipherAdmin: (id: string) => Promise<any>;
deleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise<any>;
deleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise<any>;
putMoveCiphers: (request: CipherBulkMoveRequest) => Promise<any>;
putShareCipher: (id: string, request: CipherShareRequest) => Promise<CipherResponse>;
putShareCiphers: (request: CipherBulkShareRequest) => Promise<ListResponse<CipherResponse>>;
putCipherCollections: (
): Promise<AttachmentResponse>;
abstract getAttachmentDataAdmin(
cipherId: string,
attachmentId: string,
): Promise<AttachmentResponse>;
abstract getCiphersOrganization(organizationId: string): Promise<ListResponse<CipherResponse>>;
abstract postCipher(request: CipherRequest): Promise<CipherResponse>;
abstract postCipherCreate(request: CipherCreateRequest): Promise<CipherResponse>;
abstract postCipherAdmin(request: CipherCreateRequest): Promise<CipherResponse>;
abstract putCipher(id: string, request: CipherRequest): Promise<CipherResponse>;
abstract putPartialCipher(id: string, request: CipherPartialRequest): Promise<CipherResponse>;
abstract putCipherAdmin(id: string, request: CipherRequest): Promise<CipherResponse>;
abstract deleteCipher(id: string): Promise<any>;
abstract deleteCipherAdmin(id: string): Promise<any>;
abstract deleteManyCiphers(request: CipherBulkDeleteRequest): Promise<any>;
abstract deleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise<any>;
abstract putMoveCiphers(request: CipherBulkMoveRequest): Promise<any>;
abstract putShareCipher(id: string, request: CipherShareRequest): Promise<CipherResponse>;
abstract putShareCiphers(request: CipherBulkShareRequest): Promise<ListResponse<CipherResponse>>;
abstract putCipherCollections(
id: string,
request: CipherCollectionsRequest,
) => Promise<OptionalCipherResponse>;
putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise<any>;
postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise<any>;
putDeleteCipher: (id: string) => Promise<any>;
putDeleteCipherAdmin: (id: string) => Promise<any>;
putDeleteManyCiphers: (request: CipherBulkDeleteRequest) => Promise<any>;
putDeleteManyCiphersAdmin: (request: CipherBulkDeleteRequest) => Promise<any>;
putRestoreCipher: (id: string) => Promise<CipherResponse>;
putRestoreCipherAdmin: (id: string) => Promise<CipherResponse>;
putRestoreManyCiphers: (
): Promise<OptionalCipherResponse>;
abstract putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise<any>;
abstract postPurgeCiphers(
request: SecretVerificationRequest,
organizationId?: string,
): Promise<any>;
abstract putDeleteCipher(id: string): Promise<any>;
abstract putDeleteCipherAdmin(id: string): Promise<any>;
abstract putDeleteManyCiphers(request: CipherBulkDeleteRequest): Promise<any>;
abstract putDeleteManyCiphersAdmin(request: CipherBulkDeleteRequest): Promise<any>;
abstract putRestoreCipher(id: string): Promise<CipherResponse>;
abstract putRestoreCipherAdmin(id: string): Promise<CipherResponse>;
abstract putRestoreManyCiphers(
request: CipherBulkRestoreRequest,
) => Promise<ListResponse<CipherResponse>>;
putRestoreManyCiphersAdmin: (
): Promise<ListResponse<CipherResponse>>;
abstract putRestoreManyCiphersAdmin(
request: CipherBulkRestoreRequest,
) => Promise<ListResponse<CipherResponse>>;
): Promise<ListResponse<CipherResponse>>;
postCipherAttachment: (
abstract postCipherAttachment(
id: string,
request: AttachmentRequest,
) => Promise<AttachmentUploadDataResponse>;
deleteCipherAttachment: (id: string, attachmentId: string) => Promise<any>;
deleteCipherAttachmentAdmin: (id: string, attachmentId: string) => Promise<any>;
postShareCipherAttachment: (
): Promise<AttachmentUploadDataResponse>;
abstract deleteCipherAttachment(id: string, attachmentId: string): Promise<any>;
abstract deleteCipherAttachmentAdmin(id: string, attachmentId: string): Promise<any>;
abstract postShareCipherAttachment(
id: string,
attachmentId: string,
data: FormData,
organizationId: string,
) => Promise<any>;
renewAttachmentUploadUrl: (
): Promise<any>;
abstract renewAttachmentUploadUrl(
id: string,
attachmentId: string,
) => Promise<AttachmentUploadDataResponse>;
postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise<any>;
): Promise<AttachmentUploadDataResponse>;
abstract postAttachmentFile(id: string, attachmentId: string, data: FormData): Promise<any>;
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
getCollectionAccessDetails: (
abstract getUserCollections(): Promise<ListResponse<CollectionResponse>>;
abstract getCollections(organizationId: string): Promise<ListResponse<CollectionResponse>>;
abstract getCollectionUsers(
organizationId: string,
id: string,
) => Promise<CollectionAccessDetailsResponse>;
getManyCollectionsWithAccessDetails: (
): Promise<SelectionReadOnlyResponse[]>;
abstract getCollectionAccessDetails(
organizationId: string,
id: string,
): Promise<CollectionAccessDetailsResponse>;
abstract getManyCollectionsWithAccessDetails(
orgId: string,
) => Promise<ListResponse<CollectionAccessDetailsResponse>>;
postCollection: (
): Promise<ListResponse<CollectionAccessDetailsResponse>>;
abstract postCollection(
organizationId: string,
request: CollectionRequest,
) => Promise<CollectionDetailsResponse>;
putCollection: (
): Promise<CollectionDetailsResponse>;
abstract putCollection(
organizationId: string,
id: string,
request: CollectionRequest,
) => Promise<CollectionDetailsResponse>;
deleteCollection: (organizationId: string, id: string) => Promise<any>;
deleteManyCollections: (organizationId: string, collectionIds: string[]) => Promise<any>;
): Promise<CollectionDetailsResponse>;
abstract deleteCollection(organizationId: string, id: string): Promise<any>;
abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise<any>;
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
getSync: () => Promise<SyncResponse>;
getSettingsDomains: () => Promise<DomainsResponse>;
putSettingsDomains: (request: UpdateDomainsRequest) => Promise<DomainsResponse>;
getTwoFactorProviders: () => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorOrganizationProviders: (
abstract getGroupUsers(organizationId: string, id: string): Promise<string[]>;
abstract deleteGroupUser(
organizationId: string,
) => Promise<ListResponse<TwoFactorProviderResponse>>;
getTwoFactorAuthenticator: (
id: string,
organizationUserId: string,
): Promise<any>;
abstract getSync(): Promise<SyncResponse>;
abstract getSettingsDomains(): Promise<DomainsResponse>;
abstract putSettingsDomains(request: UpdateDomainsRequest): Promise<DomainsResponse>;
abstract getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>>;
abstract getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>>;
abstract getTwoFactorAuthenticator(
request: SecretVerificationRequest,
) => Promise<TwoFactorAuthenticatorResponse>;
getTwoFactorEmail: (request: SecretVerificationRequest) => Promise<TwoFactorEmailResponse>;
getTwoFactorDuo: (request: SecretVerificationRequest) => Promise<TwoFactorDuoResponse>;
getTwoFactorOrganizationDuo: (
): Promise<TwoFactorAuthenticatorResponse>;
abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse>;
abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse>;
abstract getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
) => Promise<TwoFactorDuoResponse>;
getTwoFactorYubiKey: (request: SecretVerificationRequest) => Promise<TwoFactorYubiKeyResponse>;
getTwoFactorWebAuthn: (request: SecretVerificationRequest) => Promise<TwoFactorWebAuthnResponse>;
getTwoFactorWebAuthnChallenge: (request: SecretVerificationRequest) => Promise<ChallengeResponse>;
getTwoFactorRecover: (request: SecretVerificationRequest) => Promise<TwoFactorRecoverResponse>;
putTwoFactorAuthenticator: (
): Promise<TwoFactorDuoResponse>;
abstract getTwoFactorYubiKey(
request: SecretVerificationRequest,
): Promise<TwoFactorYubiKeyResponse>;
abstract getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse>;
abstract getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse>;
abstract getTwoFactorRecover(
request: SecretVerificationRequest,
): Promise<TwoFactorRecoverResponse>;
abstract putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
) => Promise<TwoFactorAuthenticatorResponse>;
deleteTwoFactorAuthenticator: (
): Promise<TwoFactorAuthenticatorResponse>;
abstract deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
) => Promise<TwoFactorProviderResponse>;
putTwoFactorEmail: (request: UpdateTwoFactorEmailRequest) => Promise<TwoFactorEmailResponse>;
putTwoFactorDuo: (request: UpdateTwoFactorDuoRequest) => Promise<TwoFactorDuoResponse>;
putTwoFactorOrganizationDuo: (
): Promise<TwoFactorProviderResponse>;
abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse>;
abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse>;
abstract putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
) => Promise<TwoFactorDuoResponse>;
putTwoFactorYubiKey: (
): Promise<TwoFactorDuoResponse>;
abstract putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
) => Promise<TwoFactorYubiKeyResponse>;
putTwoFactorWebAuthn: (
): Promise<TwoFactorYubiKeyResponse>;
abstract putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
) => Promise<TwoFactorWebAuthnResponse>;
deleteTwoFactorWebAuthn: (
): Promise<TwoFactorWebAuthnResponse>;
abstract deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
) => Promise<TwoFactorWebAuthnResponse>;
putTwoFactorDisable: (request: TwoFactorProviderRequest) => Promise<TwoFactorProviderResponse>;
putTwoFactorOrganizationDisable: (
): Promise<TwoFactorWebAuthnResponse>;
abstract putTwoFactorDisable(
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse>;
abstract putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
) => Promise<TwoFactorProviderResponse>;
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise<any>;
getDeviceVerificationSettings: () => Promise<DeviceVerificationResponse>;
putDeviceVerificationSettings: (
): Promise<TwoFactorProviderResponse>;
abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any>;
abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any>;
abstract getDeviceVerificationSettings(): Promise<DeviceVerificationResponse>;
abstract putDeviceVerificationSettings(
request: DeviceVerificationRequest,
) => Promise<DeviceVerificationResponse>;
): Promise<DeviceVerificationResponse>;
getCloudCommunicationsEnabled: () => Promise<boolean>;
abstract getCloudCommunicationsEnabled(): Promise<boolean>;
abstract getOrganizationConnection<TConfig extends OrganizationConnectionConfigApis>(
id: string,
type: OrganizationConnectionType,
@@ -340,136 +367,147 @@ export abstract class ApiService {
configType: { new (response: any): TConfig },
organizationConnectionId: string,
): Promise<OrganizationConnectionResponse<TConfig>>;
deleteOrganizationConnection: (id: string) => Promise<void>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
abstract deleteOrganizationConnection(id: string): Promise<void>;
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;
postProviderUserReinvite: (providerId: string, id: string) => Promise<any>;
postManyProviderUserReinvite: (
abstract getProviderUsers(
providerId: string,
): Promise<ListResponse<ProviderUserUserDetailsResponse>>;
abstract getProviderUser(providerId: string, id: string): Promise<ProviderUserResponse>;
abstract postProviderUserInvite(
providerId: string,
request: ProviderUserInviteRequest,
): Promise<any>;
abstract postProviderUserReinvite(providerId: string, id: string): Promise<any>;
abstract postManyProviderUserReinvite(
providerId: string,
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
postProviderUserAccept: (
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract postProviderUserAccept(
providerId: string,
id: string,
request: ProviderUserAcceptRequest,
) => Promise<any>;
postProviderUserConfirm: (
): Promise<any>;
abstract postProviderUserConfirm(
providerId: string,
id: string,
request: ProviderUserConfirmRequest,
) => Promise<any>;
postProviderUsersPublicKey: (
): Promise<any>;
abstract postProviderUsersPublicKey(
providerId: string,
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
postProviderUserBulkConfirm: (
): Promise<ListResponse<ProviderUserBulkPublicKeyResponse>>;
abstract postProviderUserBulkConfirm(
providerId: string,
request: ProviderUserBulkConfirmRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
putProviderUser: (
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract putProviderUser(
providerId: string,
id: string,
request: ProviderUserUpdateRequest,
) => Promise<any>;
deleteProviderUser: (organizationId: string, id: string) => Promise<any>;
deleteManyProviderUsers: (
): Promise<any>;
abstract deleteProviderUser(organizationId: string, id: string): Promise<any>;
abstract deleteManyProviderUsers(
providerId: string,
request: ProviderUserBulkRequest,
) => Promise<ListResponse<ProviderUserBulkResponse>>;
getProviderClients: (
): Promise<ListResponse<ProviderUserBulkResponse>>;
abstract getProviderClients(
providerId: string,
) => Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
postProviderAddOrganization: (
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
abstract postProviderAddOrganization(
providerId: string,
request: ProviderAddOrganizationRequest,
) => Promise<any>;
postProviderCreateOrganization: (
): Promise<any>;
abstract postProviderCreateOrganization(
providerId: string,
request: ProviderOrganizationCreateRequest,
) => Promise<ProviderOrganizationResponse>;
deleteProviderOrganization: (providerId: string, organizationId: string) => Promise<any>;
): Promise<ProviderOrganizationResponse>;
abstract deleteProviderOrganization(providerId: string, organizationId: string): Promise<any>;
getEvents: (start: string, end: string, token: string) => Promise<ListResponse<EventResponse>>;
getEventsCipher: (
abstract getEvents(
start: string,
end: string,
token: string,
): Promise<ListResponse<EventResponse>>;
abstract getEventsCipher(
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsOrganization: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsOrganization(
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsOrganizationUser: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsOrganizationUser(
organizationId: string,
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsProvider: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsProvider(
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
getEventsProviderUser: (
): Promise<ListResponse<EventResponse>>;
abstract getEventsProviderUser(
providerId: string,
id: string,
start: string,
end: string,
token: string,
) => Promise<ListResponse<EventResponse>>;
): Promise<ListResponse<EventResponse>>;
/**
* Posts events for a user
* @param request The array of events to upload
* @param userId The optional user id the events belong to. If no user id is provided the active user id is used.
*/
postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise<any>;
abstract postEventsCollect(request: EventRequest[], userId?: UserId): Promise<any>;
deleteSsoUser: (organizationId: string) => Promise<void>;
getSsoUserIdentifier: () => Promise<string>;
abstract deleteSsoUser(organizationId: string): Promise<void>;
abstract getSsoUserIdentifier(): Promise<string>;
getUserPublicKey: (id: string) => Promise<UserKeyResponse>;
abstract getUserPublicKey(id: string): Promise<UserKeyResponse>;
getHibpBreach: (username: string) => Promise<BreachAccountResponse[]>;
abstract getHibpBreach(username: string): Promise<BreachAccountResponse[]>;
postBitPayInvoice: (request: BitPayInvoiceRequest) => Promise<string>;
postSetupPayment: () => Promise<string>;
abstract postBitPayInvoice(request: BitPayInvoiceRequest): Promise<string>;
abstract postSetupPayment(): Promise<string>;
getActiveBearerToken: () => Promise<string>;
fetch: (request: Request) => Promise<Response>;
nativeFetch: (request: Request) => Promise<Response>;
abstract getActiveBearerToken(): Promise<string>;
abstract fetch(request: Request): Promise<Response>;
abstract nativeFetch(request: Request): Promise<Response>;
preValidateSso: (identifier: string) => Promise<SsoPreValidateResponse>;
abstract preValidateSso(identifier: string): Promise<SsoPreValidateResponse>;
postCreateSponsorship: (
abstract postCreateSponsorship(
sponsorshipOrgId: string,
request: OrganizationSponsorshipCreateRequest,
) => Promise<void>;
getSponsorshipSyncStatus: (
): Promise<void>;
abstract getSponsorshipSyncStatus(
sponsoredOrgId: string,
) => Promise<OrganizationSponsorshipSyncStatusResponse>;
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
postPreValidateSponsorshipToken: (
): Promise<OrganizationSponsorshipSyncStatusResponse>;
abstract deleteRemoveSponsorship(sponsoringOrgId: string): Promise<void>;
abstract postPreValidateSponsorshipToken(
sponsorshipToken: string,
) => Promise<PreValidateSponsorshipResponse>;
postRedeemSponsorship: (
): Promise<PreValidateSponsorshipResponse>;
abstract postRedeemSponsorship(
sponsorshipToken: string,
request: OrganizationSponsorshipRedeemRequest,
) => Promise<void>;
): Promise<void>;
getMasterKeyFromKeyConnector: (keyConnectorUrl: string) => Promise<KeyConnectorUserKeyResponse>;
postUserKeyToKeyConnector: (
abstract getMasterKeyFromKeyConnector(
keyConnectorUrl: string,
): Promise<KeyConnectorUserKeyResponse>;
abstract postUserKeyToKeyConnector(
keyConnectorUrl: string,
request: KeyConnectorUserKeyRequest,
) => Promise<void>;
getKeyConnectorAlive: (keyConnectorUrl: string) => Promise<void>;
getOrganizationExport: (organizationId: string) => Promise<OrganizationExportResponse>;
): Promise<void>;
abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise<void>;
abstract getOrganizationExport(organizationId: string): Promise<OrganizationExportResponse>;
}

View File

@@ -1,18 +1,16 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EventType } from "../../enums";
import { CipherView } from "../../vault/models/view/cipher.view";
export abstract class EventCollectionService {
collectMany: (
abstract collectMany(
eventType: EventType,
ciphers: CipherView[],
uploadImmediately?: boolean,
) => Promise<any>;
collect: (
): Promise<any>;
abstract collect(
eventType: EventType,
cipherId?: string,
uploadImmediately?: boolean,
organizationId?: string,
) => Promise<any>;
): Promise<any>;
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "../../types/guid";
export abstract class EventUploadService {
uploadEvents: (userId?: UserId) => Promise<void>;
abstract uploadEvents(userId?: UserId): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request";
@@ -8,19 +6,19 @@ import { OrganizationDomainResponse } from "./responses/organization-domain.resp
import { VerifiedOrganizationDomainSsoDetailsResponse } from "./responses/verified-organization-domain-sso-details.response";
export abstract class OrgDomainApiServiceAbstraction {
getAllByOrgId: (orgId: string) => Promise<Array<OrganizationDomainResponse>>;
getByOrgIdAndOrgDomainId: (
abstract getAllByOrgId(orgId: string): Promise<Array<OrganizationDomainResponse>>;
abstract getByOrgIdAndOrgDomainId(
orgId: string,
orgDomainId: string,
) => Promise<OrganizationDomainResponse>;
post: (
): Promise<OrganizationDomainResponse>;
abstract post(
orgId: string,
orgDomain: OrganizationDomainRequest,
) => Promise<OrganizationDomainResponse>;
verify: (orgId: string, orgDomainId: string) => Promise<OrganizationDomainResponse>;
delete: (orgId: string, orgDomainId: string) => Promise<any>;
getClaimedOrgDomainByEmail: (email: string) => Promise<OrganizationDomainSsoDetailsResponse>;
getVerifiedOrgDomainsByEmail: (
): Promise<OrganizationDomainResponse>;
abstract verify(orgId: string, orgDomainId: string): Promise<OrganizationDomainResponse>;
abstract delete(orgId: string, orgDomainId: string): Promise<any>;
abstract getClaimedOrgDomainByEmail(email: string): Promise<OrganizationDomainSsoDetailsResponse>;
abstract getVerifiedOrgDomainsByEmail(
email: string,
) => Promise<ListResponse<VerifiedOrganizationDomainSsoDetailsResponse>>;
): Promise<ListResponse<VerifiedOrganizationDomainSsoDetailsResponse>>;
}

View File

@@ -1,22 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { OrganizationDomainResponse } from "./responses/organization-domain.response";
export abstract class OrgDomainServiceAbstraction {
orgDomains$: Observable<OrganizationDomainResponse[]>;
abstract orgDomains$: Observable<OrganizationDomainResponse[]>;
get: (orgDomainId: string) => OrganizationDomainResponse;
abstract get(orgDomainId: string): OrganizationDomainResponse;
copyDnsTxt: (dnsTxt: string) => void;
abstract copyDnsTxt(dnsTxt: string): void;
}
// Note: this separate class is designed to hold methods that are not
// meant to be used in components (e.g., data write methods)
export abstract class OrgDomainInternalServiceAbstraction extends OrgDomainServiceAbstraction {
upsert: (orgDomains: OrganizationDomainResponse[]) => void;
replace: (orgDomains: OrganizationDomainResponse[]) => void;
clearCache: () => void;
delete: (orgDomainIds: string[]) => void;
abstract upsert(orgDomains: OrganizationDomainResponse[]): void;
abstract replace(orgDomains: OrganizationDomainResponse[]): void;
abstract clearCache(): void;
abstract delete(orgDomainIds: string[]): void;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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";
@@ -34,60 +32,66 @@ import { OrganizationKeysResponse } from "../../models/response/organization-key
import { OrganizationResponse } from "../../models/response/organization.response";
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
export class OrganizationApiServiceAbstraction {
get: (id: string) => Promise<OrganizationResponse>;
getBilling: (id: string) => Promise<BillingResponse>;
getBillingHistory: (id: string) => Promise<BillingHistoryResponse>;
getSubscription: (id: string) => Promise<OrganizationSubscriptionResponse>;
getLicense: (id: string, installationId: string) => Promise<unknown>;
getAutoEnrollStatus: (identifier: string) => Promise<OrganizationAutoEnrollStatusResponse>;
create: (request: OrganizationCreateRequest) => Promise<OrganizationResponse>;
createWithoutPayment: (
export abstract class OrganizationApiServiceAbstraction {
abstract get(id: string): Promise<OrganizationResponse>;
abstract getBilling(id: string): Promise<BillingResponse>;
abstract getBillingHistory(id: string): Promise<BillingHistoryResponse>;
abstract getSubscription(id: string): Promise<OrganizationSubscriptionResponse>;
abstract getLicense(id: string, installationId: string): Promise<unknown>;
abstract getAutoEnrollStatus(identifier: string): Promise<OrganizationAutoEnrollStatusResponse>;
abstract create(request: OrganizationCreateRequest): Promise<OrganizationResponse>;
abstract createWithoutPayment(
request: OrganizationNoPaymentMethodCreateRequest,
) => Promise<OrganizationResponse>;
createLicense: (data: FormData) => Promise<OrganizationResponse>;
save: (id: string, request: OrganizationUpdateRequest) => Promise<OrganizationResponse>;
updatePayment: (id: string, request: PaymentRequest) => Promise<void>;
upgrade: (id: string, request: OrganizationUpgradeRequest) => Promise<PaymentResponse>;
updatePasswordManagerSeats: (
): Promise<OrganizationResponse>;
abstract createLicense(data: FormData): Promise<OrganizationResponse>;
abstract save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse>;
abstract updatePayment(id: string, request: PaymentRequest): Promise<void>;
abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise<PaymentResponse>;
abstract updatePasswordManagerSeats(
id: string,
request: OrganizationSubscriptionUpdateRequest,
) => Promise<ProfileOrganizationResponse>;
updateSecretsManagerSubscription: (
): Promise<ProfileOrganizationResponse>;
abstract updateSecretsManagerSubscription(
id: string,
request: OrganizationSmSubscriptionUpdateRequest,
) => Promise<ProfileOrganizationResponse>;
updateSeats: (id: string, request: SeatRequest) => Promise<PaymentResponse>;
updateStorage: (id: string, request: StorageRequest) => Promise<PaymentResponse>;
verifyBank: (id: string, request: VerifyBankRequest) => Promise<void>;
reinstate: (id: string) => Promise<void>;
leave: (id: string) => Promise<void>;
delete: (id: string, request: SecretVerificationRequest) => Promise<void>;
deleteUsingToken: (
): Promise<ProfileOrganizationResponse>;
abstract updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse>;
abstract updateStorage(id: string, request: StorageRequest): Promise<PaymentResponse>;
abstract verifyBank(id: string, request: VerifyBankRequest): Promise<void>;
abstract reinstate(id: string): Promise<void>;
abstract leave(id: string): Promise<void>;
abstract delete(id: string, request: SecretVerificationRequest): Promise<void>;
abstract deleteUsingToken(
organizationId: string,
request: OrganizationVerifyDeleteRecoverRequest,
) => Promise<any>;
updateLicense: (id: string, data: FormData) => Promise<void>;
importDirectory: (organizationId: string, request: ImportDirectoryRequest) => Promise<void>;
getOrCreateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise<ApiKeyResponse>;
getApiKeyInformation: (
): Promise<any>;
abstract updateLicense(id: string, data: FormData): Promise<void>;
abstract importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise<void>;
abstract getOrCreateApiKey(
id: string,
request: OrganizationApiKeyRequest,
): Promise<ApiKeyResponse>;
abstract getApiKeyInformation(
id: string,
organizationApiKeyType?: OrganizationApiKeyType,
) => Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise<ApiKeyResponse>;
getTaxInfo: (id: string) => Promise<TaxInfoResponse>;
updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise<void>;
getKeys: (id: string) => Promise<OrganizationKeysResponse>;
updateKeys: (id: string, request: OrganizationKeysRequest) => Promise<OrganizationKeysResponse>;
getSso: (id: string) => Promise<OrganizationSsoResponse>;
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
selfHostedSyncLicense: (id: string) => Promise<void>;
subscribeToSecretsManager: (
): Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise<ApiKeyResponse>;
abstract getTaxInfo(id: string): Promise<TaxInfoResponse>;
abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise<void>;
abstract getKeys(id: string): Promise<OrganizationKeysResponse>;
abstract updateKeys(
id: string,
request: OrganizationKeysRequest,
): Promise<OrganizationKeysResponse>;
abstract getSso(id: string): Promise<OrganizationSsoResponse>;
abstract updateSso(id: string, request: OrganizationSsoRequest): Promise<OrganizationSsoResponse>;
abstract selfHostedSyncLicense(id: string): Promise<void>;
abstract subscribeToSecretsManager(
id: string,
request: SecretsManagerSubscribeRequest,
) => Promise<ProfileOrganizationResponse>;
updateCollectionManagement: (
): Promise<ProfileOrganizationResponse>;
abstract updateCollectionManagement(
id: string,
request: OrganizationCollectionManagementUpdateRequest,
) => Promise<OrganizationResponse>;
): Promise<OrganizationResponse>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { map, Observable } from "rxjs";
import { UserId } from "../../../types/guid";
@@ -68,20 +66,20 @@ export abstract class OrganizationService {
* Publishes state for all organizations under the specified user.
* @returns An observable list of organizations
*/
organizations$: (userId: UserId) => Observable<Organization[]>;
abstract organizations$(userId: UserId): Observable<Organization[]>;
// @todo Clean these up. Continuing to expand them is not recommended.
// @see https://bitwarden.atlassian.net/browse/AC-2252
memberOrganizations$: (userId: UserId) => Observable<Organization[]>;
abstract memberOrganizations$(userId: UserId): Observable<Organization[]>;
/**
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
*/
canManageSponsorships$: (userId: UserId) => Observable<boolean>;
abstract canManageSponsorships$(userId: UserId): Observable<boolean>;
/**
* Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
*/
familySponsorshipAvailable$: (userId: UserId) => Observable<boolean>;
hasOrganizations: (userId: UserId) => Observable<boolean>;
abstract familySponsorshipAvailable$(userId: UserId): Observable<boolean>;
abstract hasOrganizations(userId: UserId): Observable<boolean>;
}
/**
@@ -96,7 +94,7 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio
* @param organization The organization state being saved.
* @param userId The userId to replace state for.
*/
upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise<void>;
abstract upsert(OrganizationData: OrganizationData, userId: UserId): Promise<void>;
/**
* Replaces state for the entire registered organization list for the specified user.
@@ -107,5 +105,8 @@ export abstract class InternalOrganizationServiceAbstraction extends Organizatio
* user.
* @param userId The userId to replace state for.
*/
replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise<void>;
abstract replace(
organizations: { [id: string]: OrganizationData },
userId: UserId,
): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
@@ -7,8 +5,8 @@ import { ProviderData } from "../models/data/provider.data";
import { Provider } from "../models/domain/provider";
export abstract class ProviderService {
get$: (id: string) => Observable<Provider>;
get: (id: string) => Promise<Provider>;
getAll: () => Promise<Provider[]>;
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;
abstract get$(id: string): Observable<Provider>;
abstract get(id: string): Promise<Provider>;
abstract getAll(): Promise<Provider[]>;
abstract save(providers: { [id: string]: ProviderData }, userId?: UserId): Promise<any>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
@@ -7,21 +5,23 @@ import { ProviderUpdateRequest } from "../../models/request/provider/provider-up
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
import { ProviderResponse } from "../../models/response/provider/provider.response";
export class ProviderApiServiceAbstraction {
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
getProvider: (id: string) => Promise<ProviderResponse>;
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
providerRecoverDeleteToken: (
export abstract class ProviderApiServiceAbstraction {
abstract postProviderSetup(id: string, request: ProviderSetupRequest): Promise<ProviderResponse>;
abstract getProvider(id: string): Promise<ProviderResponse>;
abstract putProvider(id: string, request: ProviderUpdateRequest): Promise<ProviderResponse>;
abstract providerRecoverDeleteToken(
organizationId: string,
request: ProviderVerifyRecoverDeleteRequest,
) => Promise<any>;
deleteProvider: (id: string) => Promise<void>;
getProviderAddableOrganizations: (providerId: string) => Promise<AddableOrganizationResponse[]>;
addOrganizationToProvider: (
): Promise<any>;
abstract deleteProvider(id: string): Promise<void>;
abstract getProviderAddableOrganizations(
providerId: string,
): Promise<AddableOrganizationResponse[]>;
abstract addOrganizationToProvider(
providerId: string,
request: {
key: string;
organizationId: string;
},
) => Promise<void>;
): Promise<void>;
}

View File

@@ -89,8 +89,7 @@ export class DefaultPolicyService implements PolicyService {
const policies$ = policies ? of(policies) : this.policies$(userId);
return policies$.pipe(
map((obsPolicies) => {
// TODO: replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies)) once
// FeatureFlag.PM16117_ChangeExistingPasswordRefactor is removed.
// TODO ([PM-23777]): replace with this.combinePoliciesIntoMasterPasswordPolicyOptions(obsPolicies))
let enforcedOptions: MasterPasswordPolicyOptions | undefined = undefined;
const filteredPolicies =
obsPolicies.filter((p) => p.type === PolicyType.MasterPassword) ?? [];

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
@@ -35,20 +33,20 @@ export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
}
export abstract class AccountService {
accounts$: Observable<Record<UserId, AccountInfo>>;
abstract accounts$: Observable<Record<UserId, AccountInfo>>;
activeAccount$: Observable<Account | null>;
abstract activeAccount$: Observable<Account | null>;
/**
* Observable of the last activity time for each account.
*/
accountActivity$: Observable<Record<UserId, Date>>;
abstract accountActivity$: Observable<Record<UserId, Date>>;
/** Observable of the new device login verification property for the account. */
accountVerifyNewDeviceLogin$: Observable<boolean>;
abstract accountVerifyNewDeviceLogin$: Observable<boolean>;
/** Account list in order of descending recency */
sortedUserIds$: Observable<UserId[]>;
abstract sortedUserIds$: Observable<UserId[]>;
/** Next account that is not the current active account */
nextUpAccount$: Observable<Account>;
abstract nextUpAccount$: Observable<Account>;
/**
* Updates the `accounts$` observable with the new account data.
*

View File

@@ -1,6 +1,4 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export abstract class AnonymousHubService {
createHubConnection: (token: string) => Promise<void>;
stopHubConnection: () => Promise<void>;
abstract createHubConnection(token: string): Promise<void>;
abstract stopHubConnection(): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
@@ -9,7 +7,7 @@ export abstract class AvatarService {
* An observable monitoring the active user's avatar color.
* The observable updates when the avatar color changes.
*/
avatarColor$: Observable<string | null>;
abstract avatarColor$: Observable<string | null>;
/**
* Sets the avatar color of the active user
*

View File

@@ -1,47 +1,45 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "../../models/response/list.response";
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
export abstract class DevicesApiServiceAbstraction {
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
abstract getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean>;
getDeviceByIdentifier: (deviceIdentifier: string) => Promise<DeviceResponse>;
abstract getDeviceByIdentifier(deviceIdentifier: string): Promise<DeviceResponse>;
getDevices: () => Promise<ListResponse<DeviceResponse>>;
abstract getDevices(): Promise<ListResponse<DeviceResponse>>;
updateTrustedDeviceKeys: (
abstract updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string,
) => Promise<DeviceResponse>;
): Promise<DeviceResponse>;
updateTrust: (
abstract updateTrust(
updateDevicesTrustRequestModel: UpdateDevicesTrustRequest,
deviceIdentifier: string,
) => Promise<void>;
): Promise<void>;
getDeviceKeys: (deviceIdentifier: string) => Promise<ProtectedDeviceResponse>;
abstract getDeviceKeys(deviceIdentifier: string): Promise<ProtectedDeviceResponse>;
/**
* Notifies the server that the device has a device key, but didn't receive any associated decryption keys.
* Note: For debugging purposes only.
* @param deviceIdentifier - current device identifier
*/
postDeviceTrustLoss: (deviceIdentifier: string) => Promise<void>;
abstract postDeviceTrustLoss(deviceIdentifier: string): Promise<void>;
/**
* Deactivates a device
* @param deviceId - The device ID
*/
deactivateDevice: (deviceId: string) => Promise<void>;
abstract deactivateDevice(deviceId: string): Promise<void>;
/**
* Removes trust from a list of devices
* @param deviceIds - The device IDs to be untrusted
*/
untrustDevices: (deviceIds: string[]) => Promise<void>;
abstract untrustDevices(deviceIds: string[]): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { VaultTimeout, VaultTimeoutAction } from "../../key-management/vault-timeout";
@@ -27,20 +25,20 @@ export abstract class TokenService {
*
* @returns A promise that resolves with the SetTokensResult containing the tokens that were set.
*/
setTokens: (
abstract setTokens(
accessToken: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
refreshToken?: string,
clientIdClientSecret?: [string, string],
) => Promise<SetTokensResult>;
): Promise<SetTokensResult>;
/**
* Clears the access token, refresh token, API Key Client ID, and API Key Client Secret out of memory, disk, and secure storage if supported.
* @param userId The optional user id to clear the tokens for; if not provided, the active user id is used.
* @returns A promise that resolves when the tokens have been cleared.
*/
clearTokens: (userId?: UserId) => Promise<void>;
abstract clearTokens(userId?: UserId): Promise<void>;
/**
* Sets the access token in memory or disk based on the given vaultTimeoutAction and vaultTimeout
@@ -51,11 +49,11 @@ export abstract class TokenService {
* @param vaultTimeout The timeout for the vault.
* @returns A promise that resolves with the access token that has been set.
*/
setAccessToken: (
abstract setAccessToken(
accessToken: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
) => Promise<string>;
): Promise<string>;
// TODO: revisit having this public clear method approach once the state service is fully deprecated.
/**
@@ -67,21 +65,21 @@ export abstract class TokenService {
* pass in the vaultTimeoutAction and vaultTimeout.
* This avoids a circular dependency between the StateService, TokenService, and VaultTimeoutSettingsService.
*/
clearAccessToken: (userId?: UserId) => Promise<void>;
abstract clearAccessToken(userId?: UserId): Promise<void>;
/**
* Gets the access token
* @param userId - The optional user id to get the access token for; if not provided, the active user is used.
* @returns A promise that resolves with the access token or null.
*/
getAccessToken: (userId?: UserId) => Promise<string | null>;
abstract getAccessToken(userId?: UserId): Promise<string | null>;
/**
* Gets the refresh token.
* @param userId - The optional user id to get the refresh token for; if not provided, the active user is used.
* @returns A promise that resolves with the refresh token or null.
*/
getRefreshToken: (userId?: UserId) => Promise<string | null>;
abstract getRefreshToken(userId?: UserId): Promise<string | null>;
/**
* Sets the API Key Client ID for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout.
@@ -90,18 +88,18 @@ export abstract class TokenService {
* @param vaultTimeout The timeout for the vault.
* @returns A promise that resolves with the API Key Client ID that has been set.
*/
setClientId: (
abstract setClientId(
clientId: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
userId?: UserId,
) => Promise<string>;
): Promise<string>;
/**
* Gets the API Key Client ID for the active user.
* @returns A promise that resolves with the API Key Client ID or undefined
*/
getClientId: (userId?: UserId) => Promise<string | undefined>;
abstract getClientId(userId?: UserId): Promise<string | undefined>;
/**
* Sets the API Key Client Secret for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout.
@@ -110,18 +108,18 @@ export abstract class TokenService {
* @param vaultTimeout The timeout for the vault.
* @returns A promise that resolves with the client secret that has been set.
*/
setClientSecret: (
abstract setClientSecret(
clientSecret: string,
vaultTimeoutAction: VaultTimeoutAction,
vaultTimeout: VaultTimeout,
userId?: UserId,
) => Promise<string>;
): Promise<string>;
/**
* Gets the API Key Client Secret for the active user.
* @returns A promise that resolves with the API Key Client Secret or undefined
*/
getClientSecret: (userId?: UserId) => Promise<string | undefined>;
abstract getClientSecret(userId?: UserId): Promise<string | undefined>;
/**
* Sets the two factor token for the given email in global state.
@@ -131,21 +129,21 @@ export abstract class TokenService {
* @param twoFactorToken The two factor token to set.
* @returns A promise that resolves when the two factor token has been set.
*/
setTwoFactorToken: (email: string, twoFactorToken: string) => Promise<void>;
abstract setTwoFactorToken(email: string, twoFactorToken: string): Promise<void>;
/**
* Gets the two factor token for the given email.
* @param email The email to get the two factor token for.
* @returns A promise that resolves with the two factor token for the given email or null if it isn't found.
*/
getTwoFactorToken: (email: string) => Promise<string | null>;
abstract getTwoFactorToken(email: string): Promise<string | null>;
/**
* Clears the two factor token for the given email out of global state.
* @param email The email to clear the two factor token for.
* @returns A promise that resolves when the two factor token has been cleared.
*/
clearTwoFactorToken: (email: string) => Promise<void>;
abstract clearTwoFactorToken(email: string): Promise<void>;
/**
* Decodes the access token.
@@ -153,13 +151,13 @@ export abstract class TokenService {
* If null, the currently active user's token is used.
* @returns A promise that resolves with the decoded access token.
*/
decodeAccessToken: (tokenOrUserId?: string | UserId) => Promise<DecodedAccessToken>;
abstract decodeAccessToken(tokenOrUserId?: string | UserId): Promise<DecodedAccessToken>;
/**
* Gets the expiration date for the access token. Returns if token can't be decoded or has no expiration
* @returns A promise that resolves with the expiration date for the access token.
*/
getTokenExpirationDate: () => Promise<Date | null>;
abstract getTokenExpirationDate(): Promise<Date | null>;
/**
* Calculates the adjusted time in seconds until the access token expires, considering an optional offset.
@@ -170,58 +168,58 @@ export abstract class TokenService {
* based on the actual expiration.
* @returns {Promise<number>} Promise resolving to the adjusted seconds remaining.
*/
tokenSecondsRemaining: (offsetSeconds?: number) => Promise<number>;
abstract tokenSecondsRemaining(offsetSeconds?: number): Promise<number>;
/**
* Checks if the access token needs to be refreshed.
* @param {number} [minutes=5] - Optional number of minutes before the access token expires to consider refreshing it.
* @returns A promise that resolves with a boolean indicating if the access token needs to be refreshed.
*/
tokenNeedsRefresh: (minutes?: number) => Promise<boolean>;
abstract tokenNeedsRefresh(minutes?: number): Promise<boolean>;
/**
* Gets the user id for the active user from the access token.
* @returns A promise that resolves with the user id for the active user.
* @deprecated Use AccountService.activeAccount$ instead.
*/
getUserId: () => Promise<UserId>;
abstract getUserId(): Promise<UserId>;
/**
* Gets the email for the active user from the access token.
* @returns A promise that resolves with the email for the active user.
* @deprecated Use AccountService.activeAccount$ instead.
*/
getEmail: () => Promise<string>;
abstract getEmail(): Promise<string>;
/**
* Gets the email verified status for the active user from the access token.
* @returns A promise that resolves with the email verified status for the active user.
*/
getEmailVerified: () => Promise<boolean>;
abstract getEmailVerified(): Promise<boolean>;
/**
* Gets the name for the active user from the access token.
* @returns A promise that resolves with the name for the active user.
* @deprecated Use AccountService.activeAccount$ instead.
*/
getName: () => Promise<string>;
abstract getName(): Promise<string>;
/**
* Gets the issuer for the active user from the access token.
* @returns A promise that resolves with the issuer for the active user.
*/
getIssuer: () => Promise<string>;
abstract getIssuer(): Promise<string>;
/**
* Gets whether or not the user authenticated via an external mechanism.
* @param userId The optional user id to check for external authN status; if not provided, the active user is used.
* @returns A promise that resolves with a boolean representing the user's external authN status.
*/
getIsExternal: (userId: UserId) => Promise<boolean>;
abstract getIsExternal(userId: UserId): Promise<boolean>;
/** Gets the active or passed in user's security stamp */
getSecurityStamp: (userId?: UserId) => Promise<string | null>;
abstract getSecurityStamp(userId?: UserId): Promise<string | null>;
/** Sets the security stamp for the active or passed in user */
setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise<void>;
abstract setSecurityStamp(securityStamp: string, userId?: UserId): Promise<void>;
}

View File

@@ -1,13 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response";
export abstract class UserVerificationApiServiceAbstraction {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postAccountRequestOTP: () => Promise<void>;
postAccountVerifyPassword: (
abstract postAccountVerifyOTP(request: VerifyOTPRequest): Promise<void>;
abstract postAccountRequestOTP(): Promise<void>;
abstract postAccountVerifyPassword(
request: SecretVerificationRequest,
) => Promise<MasterPasswordPolicyResponse>;
): Promise<MasterPasswordPolicyResponse>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "../../../types/guid";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { UserVerificationOptions } from "../../types/user-verification-options";
@@ -16,9 +14,9 @@ export abstract class UserVerificationService {
* @param verificationType Type of verification to restrict the options to
* @returns Available verification options for the user
*/
getAvailableVerificationOptions: (
abstract getAvailableVerificationOptions(
verificationType: keyof UserVerificationOptions,
) => Promise<UserVerificationOptions>;
): Promise<UserVerificationOptions>;
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)
@@ -26,11 +24,11 @@ export abstract class UserVerificationService {
* @param alreadyHashed Whether the master password is already hashed
* @throws Error if the verification data is invalid
*/
buildRequest: <T extends SecretVerificationRequest>(
abstract buildRequest<T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean,
) => Promise<T>;
): Promise<T>;
/**
* Verifies the user using the provided verification data.
* PIN or biometrics are verified client-side.
@@ -39,11 +37,11 @@ export abstract class UserVerificationService {
* @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
* @throws Error if the verification data is invalid or the verification fails
*/
verifyUser: (verification: Verification) => Promise<boolean>;
abstract verifyUser(verification: Verification): Promise<boolean>;
/**
* Request a one-time password (OTP) to be sent to the user's email
*/
requestOTP: () => Promise<void>;
abstract requestOTP(): Promise<void>;
/**
* Check if user has master password or can only use passwordless technologies to log in
* Note: This only checks the server, not the local state
@@ -51,13 +49,13 @@ export abstract class UserVerificationService {
* @returns True if the user has a master password
* @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead
*/
hasMasterPassword: (userId?: string) => Promise<boolean>;
abstract hasMasterPassword(userId?: string): Promise<boolean>;
/**
* Check if the user has a master password and has used it during their current session
* @param userId The user id to check. If not provided, the current user id used
* @returns True if the user has a master password and has used it in the current session
*/
hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise<boolean>;
abstract hasMasterPasswordAndMasterKeyHash(userId?: string): Promise<boolean>;
/**
* Verifies the user using the provided master password.
* Attempts to verify client-side first, then server-side if necessary.
@@ -68,9 +66,9 @@ export abstract class UserVerificationService {
* @throws Error if the master password is invalid
* @returns An object containing the master key, and master password policy options if verified on server.
*/
verifyUserByMasterPassword: (
abstract verifyUserByMasterPassword(
verification: MasterPasswordVerification,
userId: UserId,
email: string,
) => Promise<MasterPasswordVerificationResponse>;
): Promise<MasterPasswordVerificationResponse>;
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CredentialAssertionOptionsResponse } from "../../services/webauthn-login/response/credential-assertion-options.response";
export class WebAuthnLoginApiServiceAbstraction {
getCredentialAssertionOptions: () => Promise<CredentialAssertionOptionsResponse>;
export abstract class WebAuthnLoginApiServiceAbstraction {
abstract getCredentialAssertionOptions(): Promise<CredentialAssertionOptionsResponse>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { PrfKey } from "../../../types/key";
/**
@@ -9,11 +7,11 @@ export abstract class WebAuthnLoginPrfKeyServiceAbstraction {
/**
* Get the salt used to generate the PRF-output used when logging in with WebAuthn.
*/
getLoginWithPrfSalt: () => Promise<ArrayBuffer>;
abstract getLoginWithPrfSalt(): Promise<ArrayBuffer>;
/**
* Create a symmetric key from the PRF-output by stretching it.
* This should be used as `ExternalKey` with `RotateableKeySet`.
*/
createSymmetricKeyFromPrf: (prf: ArrayBuffer) => Promise<PrfKey>;
abstract createSymmetricKeyFromPrf(prf: ArrayBuffer): Promise<PrfKey>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { AuthResult } from "../../models/domain/auth-result";
import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion.view";
@@ -14,7 +12,7 @@ export abstract class WebAuthnLoginServiceAbstraction {
* (whether FIDO2 user verification is required, the relying party id, timeout duration for the process to complete, etc.)
* for the authenticator.
*/
getCredentialAssertionOptions: () => Promise<WebAuthnLoginCredentialAssertionOptionsView>;
abstract getCredentialAssertionOptions(): Promise<WebAuthnLoginCredentialAssertionOptionsView>;
/**
* Asserts the credential. This involves user interaction with the authenticator
@@ -27,9 +25,9 @@ export abstract class WebAuthnLoginServiceAbstraction {
* @returns {WebAuthnLoginCredentialAssertionView} The assertion obtained from the authenticator.
* If the assertion is not successfully obtained, it returns undefined.
*/
assertCredential: (
abstract assertCredential(
credentialAssertionOptions: WebAuthnLoginCredentialAssertionOptionsView,
) => Promise<WebAuthnLoginCredentialAssertionView | undefined>;
): Promise<WebAuthnLoginCredentialAssertionView | undefined>;
/**
* Logs the user in using the assertion obtained from the authenticator.
@@ -39,5 +37,5 @@ export abstract class WebAuthnLoginServiceAbstraction {
* @param {WebAuthnLoginCredentialAssertionView} assertion - The assertion obtained from the authenticator
* that needs to be validated for login.
*/
logIn: (assertion: WebAuthnLoginCredentialAssertionView) => Promise<AuthResult>;
abstract logIn(assertion: WebAuthnLoginCredentialAssertionView): Promise<AuthResult>;
}

View File

@@ -1,11 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
BillingInvoiceResponse,
BillingTransactionResponse,
} from "../../models/response/billing.response";
export class AccountBillingApiServiceAbstraction {
getBillingInvoices: (status?: string, startAfter?: string) => Promise<BillingInvoiceResponse[]>;
getBillingTransactions: (startAfter?: string) => Promise<BillingTransactionResponse[]>;
export abstract class AccountBillingApiServiceAbstraction {
abstract getBillingInvoices(
status?: string,
startAfter?: string,
): Promise<BillingInvoiceResponse[]>;
abstract getBillingTransactions(startAfter?: string): Promise<BillingTransactionResponse[]>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../../types/guid";

View File

@@ -1,6 +1,3 @@
// 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 { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
@@ -20,78 +17,78 @@ import { PaymentMethodResponse } from "../models/response/payment-method.respons
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
export abstract class BillingApiServiceAbstraction {
cancelOrganizationSubscription: (
abstract cancelOrganizationSubscription(
organizationId: string,
request: SubscriptionCancellationRequest,
) => Promise<void>;
): Promise<void>;
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
abstract cancelPremiumUserSubscription(request: SubscriptionCancellationRequest): Promise<void>;
createProviderClientOrganization: (
abstract createProviderClientOrganization(
providerId: string,
request: CreateClientOrganizationRequest,
) => Promise<void>;
): Promise<void>;
createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise<string>;
abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise<string>;
getOrganizationBillingMetadata: (
abstract getOrganizationBillingMetadata(
organizationId: string,
) => Promise<OrganizationBillingMetadataResponse>;
): Promise<OrganizationBillingMetadataResponse>;
getOrganizationPaymentMethod: (organizationId: string) => Promise<PaymentMethodResponse>;
abstract getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise<string>;
abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise<string>;
getProviderClientOrganizations: (
abstract getProviderClientOrganizations(
providerId: string,
) => Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>>;
getProviderInvoices: (providerId: string) => Promise<InvoicesResponse>;
abstract getProviderInvoices(providerId: string): Promise<InvoicesResponse>;
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
abstract getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse>;
getProviderTaxInformation: (providerId: string) => Promise<TaxInfoResponse>;
abstract getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse>;
updateOrganizationPaymentMethod: (
abstract updateOrganizationPaymentMethod(
organizationId: string,
request: UpdatePaymentMethodRequest,
) => Promise<void>;
): Promise<void>;
updateOrganizationTaxInformation: (
abstract updateOrganizationTaxInformation(
organizationId: string,
request: ExpandedTaxInfoUpdateRequest,
) => Promise<void>;
): Promise<void>;
updateProviderClientOrganization: (
abstract updateProviderClientOrganization(
providerId: string,
organizationId: string,
request: UpdateClientOrganizationRequest,
) => Promise<any>;
): Promise<any>;
updateProviderPaymentMethod: (
abstract updateProviderPaymentMethod(
providerId: string,
request: UpdatePaymentMethodRequest,
) => Promise<void>;
): Promise<void>;
updateProviderTaxInformation: (
abstract updateProviderTaxInformation(
providerId: string,
request: ExpandedTaxInfoUpdateRequest,
) => Promise<void>;
): Promise<void>;
verifyOrganizationBankAccount: (
abstract verifyOrganizationBankAccount(
organizationId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
): Promise<void>;
verifyProviderBankAccount: (
abstract verifyProviderBankAccount(
providerId: string,
request: VerifyBankAccountRequest,
) => Promise<void>;
): Promise<void>;
restartSubscription: (
abstract restartSubscription(
organizationId: string,
request: OrganizationCreateRequest,
) => Promise<void>;
): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -49,20 +47,22 @@ export type SubscriptionInformation = {
};
export abstract class OrganizationBillingServiceAbstraction {
getPaymentSource: (organizationId: string) => Promise<PaymentSourceResponse>;
abstract getPaymentSource(organizationId: string): Promise<PaymentSourceResponse>;
purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
purchaseSubscriptionNoPaymentMethod: (
abstract purchaseSubscription(
subscription: SubscriptionInformation,
) => Promise<OrganizationResponse>;
): Promise<OrganizationResponse>;
startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
abstract purchaseSubscriptionNoPaymentMethod(
subscription: SubscriptionInformation,
): Promise<OrganizationResponse>;
restartSubscription: (
abstract startFree(subscription: SubscriptionInformation): Promise<OrganizationResponse>;
abstract restartSubscription(
organizationId: string,
subscription: SubscriptionInformation,
) => Promise<void>;
): Promise<void>;
/**
* Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.

View File

@@ -1,3 +1,4 @@
import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request";
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
import {
@@ -28,4 +29,9 @@ export abstract class OrganizationBillingApiServiceAbstraction {
organizationKey: string;
},
) => Promise<string>;
abstract changeSubscriptionFrequency: (
organizationId: string,
request: ChangePlanFrequencyRequest,
) => Promise<void>;
}

View File

@@ -0,0 +1,9 @@
import { PlanType } from "../../enums";
export class ChangePlanFrequencyRequest {
newPlanType: PlanType;
constructor(newPlanType?: PlanType) {
this.newPlanType = newPlanType!;
}
}

View File

@@ -1,3 +1,4 @@
import { ChangePlanFrequencyRequest } from "@bitwarden/common/billing/models/request/change-plan-frequency.request";
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
import { ApiService } from "../../../abstractions/api.service";
@@ -83,4 +84,17 @@ export class OrganizationBillingApiService implements OrganizationBillingApiServ
return response as string;
}
async changeSubscriptionFrequency(
organizationId: string,
request: ChangePlanFrequencyRequest,
): Promise<void> {
return await this.apiService.send(
"POST",
"/organizations/" + organizationId + "/billing/change-frequency",
request,
true,
false,
);
}
}

View File

@@ -14,8 +14,6 @@ export enum FeatureFlag {
CreateDefaultLocation = "pm-19467-create-default-location",
/* Auth */
PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor",
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
/* Autofill */
@@ -35,9 +33,6 @@ export enum FeatureFlag {
AllowTrialLengthZero = "pm-20322-allow-trial-length-0",
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
/* Data Insights and Reporting */
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
@@ -49,11 +44,15 @@ export enum FeatureFlag {
/* Tools */
DesktopSendUIRefresh = "desktop-send-ui-refresh",
/* DIRT */
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
/* Vault */
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
CipherKeyEncryption = "cipher-key-encryption",
EndUserNotifications = "pm-10609-end-user-notifications",
RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy",
@@ -88,12 +87,12 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.WindowsDesktopAutotype]: FALSE,
/* Data Insights and Reporting */
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
/* Tools */
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
/* DIRT */
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
/* Vault */
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
@@ -103,10 +102,9 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.RemoveCardItemTypePolicy]: FALSE,
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
[FeatureFlag.PM19315EndUserActivationMvp]: FALSE,
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
/* Auth */
[FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE,
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE,
/* Billing */

View File

@@ -41,7 +41,6 @@ export abstract class CryptoFunctionService {
algorithm: "sha1" | "sha256" | "sha512",
): Promise<Uint8Array | string>;
abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise<boolean>;
abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array>;
abstract aesDecryptFastParameters(
data: string,
iv: string,

View File

@@ -7,20 +7,6 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
import { EncString } from "../models/enc-string";
export abstract class EncryptService {
/**
* @deprecated
* Encrypts a string or Uint8Array to an EncString
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
/**
* @deprecated
* Encrypts a value to a Uint8Array
* @param plainValue - The value to encrypt
* @param key - The key to encrypt the value with
*/
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
/**
* @deprecated
* Decrypts an EncString to a string

View File

@@ -153,6 +153,10 @@ export class EncString implements Encrypted {
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
}
/**
* @deprecated - This function is deprecated. Use EncryptService.decryptString instead.
* @returns - The decrypted string, or `[error: cannot decrypt]` if decryption fails.
*/
async decrypt(
orgId: string | null,
key: SymmetricCryptoKey | null = null,

View File

@@ -1,170 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import * as rxjs from "rxjs";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { LogService } from "../../../platform/abstractions/log.service";
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { buildSetConfigMessage } from "../types/worker-command.type";
import { BulkEncryptServiceImplementation } from "./bulk-encrypt.service.implementation";
describe("BulkEncryptServiceImplementation", () => {
const cryptoFunctionService = mock<CryptoFunctionService>();
const logService = mock<LogService>();
let sut: BulkEncryptServiceImplementation;
beforeEach(() => {
sut = new BulkEncryptServiceImplementation(cryptoFunctionService, logService);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("decryptItems", () => {
const key = mock<SymmetricCryptoKey>();
const serverConfig = mock<ServerConfig>();
const mockWorker = mock<Worker>();
let globalWindow: any;
beforeEach(() => {
globalWindow = global.window;
// Mock creating a worker.
global.Worker = jest.fn().mockImplementation(() => mockWorker);
global.URL = jest.fn().mockImplementation(() => "url") as unknown as typeof URL;
global.URL.createObjectURL = jest.fn().mockReturnValue("blob:url");
global.URL.revokeObjectURL = jest.fn();
global.URL.canParse = jest.fn().mockReturnValue(true);
// Mock the workers returned response.
const mockMessageEvent = {
id: "mock-guid",
data: ["decrypted1", "decrypted2"],
};
const mockMessageEvent$ = rxjs.from([mockMessageEvent]);
jest.spyOn(rxjs, "fromEvent").mockReturnValue(mockMessageEvent$);
});
afterEach(() => {
global.window = globalWindow;
});
it("throws error if key is null", async () => {
const nullKey = null as unknown as SymmetricCryptoKey;
await expect(sut.decryptItems([], nullKey)).rejects.toThrow("No encryption key provided.");
});
it("returns an empty array when items is null", async () => {
const result = await sut.decryptItems(null as any, key);
expect(result).toEqual([]);
});
it("returns an empty array when items is empty", async () => {
const result = await sut.decryptItems([], key);
expect(result).toEqual([]);
});
it("decrypts items sequentially when window is undefined", async () => {
// Make global window undefined.
delete (global as any).window;
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
const result = await sut.decryptItems(mockItems, key);
expect(logService.info).toHaveBeenCalledWith(
"Window not available in BulkEncryptService, decrypting sequentially",
);
expect(result).toEqual(["item1", "item2"]);
expect(mockItems[0].decrypt).toHaveBeenCalledWith(key);
expect(mockItems[1].decrypt).toHaveBeenCalledWith(key);
});
it("uses workers for decryption when window is available", async () => {
const mockDecryptedItems = ["decrypted1", "decrypted2"];
jest
.spyOn<any, any>(sut, "getDecryptedItemsFromWorkers")
.mockResolvedValue(mockDecryptedItems);
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
const result = await sut.decryptItems(mockItems, key);
expect(sut["getDecryptedItemsFromWorkers"]).toHaveBeenCalledWith(mockItems, key);
expect(result).toEqual(mockDecryptedItems);
});
it("creates new worker when none exist", async () => {
(sut as any).currentServerConfig = undefined;
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
await sut.decryptItems(mockItems, key);
expect(global.Worker).toHaveBeenCalled();
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
it("sends a SetConfigMessage to the new worker when there is a current server config", async () => {
(sut as any).currentServerConfig = serverConfig;
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
await sut.decryptItems(mockItems, key);
expect(global.Worker).toHaveBeenCalled();
expect(mockWorker.postMessage).toHaveBeenCalledTimes(2);
expect(mockWorker.postMessage).toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
it("does not create worker if one exists", async () => {
(sut as any).currentServerConfig = serverConfig;
(sut as any).workers = [mockWorker];
const mockItems = [createMockDecryptable("item1"), createMockDecryptable("item2")];
await sut.decryptItems(mockItems, key);
expect(global.Worker).not.toHaveBeenCalled();
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
});
describe("onServerConfigChange", () => {
it("updates internal currentServerConfig to new config", () => {
const newConfig = mock<ServerConfig>();
sut.onServerConfigChange(newConfig);
expect((sut as any).currentServerConfig).toBe(newConfig);
});
it("does send a SetConfigMessage to workers when there is a worker", () => {
const newConfig = mock<ServerConfig>();
const mockWorker = mock<Worker>();
(sut as any).workers = [mockWorker];
sut.onServerConfigChange(newConfig);
expect(mockWorker.postMessage).toHaveBeenCalledWith(buildSetConfigMessage({ newConfig }));
});
});
});
function createMockDecryptable<T extends InitializerMetadata>(
returnValue: any,
): MockProxy<Decryptable<T>> {
const mockDecryptable = mock<Decryptable<T>>();
mockDecryptable.decrypt.mockResolvedValue(returnValue);
return mockDecryptable;
}

View File

@@ -1,38 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, fromEvent, filter, map, takeUntil, defaultIfEmpty, Subject } from "rxjs";
import { Jsonify } from "type-fest";
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
import {
DefaultFeatureFlagValue,
FeatureFlag,
getFeatureFlagValue,
} from "../../../enums/feature-flag.enum";
import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
const workerTTL = 60000; // 1 minute
const maxWorkers = 8;
const minNumberOfItemsForMultithreading = 400;
/**
* @deprecated Will be deleted in an immediate subsequent PR
*/
export class BulkEncryptServiceImplementation implements BulkEncryptService {
private workers: Worker[] = [];
private timeout: any;
private currentServerConfig: ServerConfig | undefined = undefined;
protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
private clear$ = new Subject<void>();
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService,
@@ -54,139 +35,12 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
return [];
}
if (typeof window === "undefined" || this.useSDKForDecryption) {
this.logService.info("Window not available in BulkEncryptService, decrypting sequentially");
const results = [];
for (let i = 0; i < items.length; i++) {
results.push(await items[i].decrypt(key));
}
return results;
}
const decryptedItems = await this.getDecryptedItemsFromWorkers(items, key);
return decryptedItems;
}
onServerConfigChange(newConfig: ServerConfig): void {
this.currentServerConfig = newConfig;
this.useSDKForDecryption = getFeatureFlagValue(newConfig, FeatureFlag.UseSDKForDecryption);
this.updateWorkerServerConfigs(newConfig);
}
/**
* Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items
* faster without interrupting other operations (e.g. updating UI).
*/
private async getDecryptedItemsFromWorkers<T extends InitializerMetadata>(
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (items == null || items.length < 1) {
return [];
}
this.clearTimeout();
const hardwareConcurrency = navigator.hardwareConcurrency || 1;
let numberOfWorkers = Math.min(hardwareConcurrency, maxWorkers);
if (items.length < minNumberOfItemsForMultithreading) {
numberOfWorkers = 1;
}
this.logService.info(
`Starting decryption using multithreading with ${numberOfWorkers} workers for ${items.length} items`,
);
if (this.workers.length == 0) {
for (let i = 0; i < numberOfWorkers; i++) {
this.workers.push(
new Worker(
new URL(
/* webpackChunkName: 'encrypt-worker' */
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
import.meta.url,
),
),
);
}
if (this.currentServerConfig != undefined) {
this.updateWorkerServerConfigs(this.currentServerConfig);
}
}
const itemsPerWorker = Math.floor(items.length / this.workers.length);
const results = [];
for (const [i, worker] of this.workers.entries()) {
const start = i * itemsPerWorker;
const end = start + itemsPerWorker;
const itemsForWorker = items.slice(start, end);
// push the remaining items to the last worker
if (i == this.workers.length - 1) {
itemsForWorker.push(...items.slice(end));
}
const id = Utils.newGuid();
const request = buildDecryptMessage({
id,
items: itemsForWorker,
key: key,
});
worker.postMessage(request);
results.push(
firstValueFrom(
fromEvent(worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === id),
map((response) => JSON.parse(response.data.items)),
map((items) =>
items.map((jsonItem: Jsonify<T>) => {
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
return initializer(jsonItem);
}),
),
takeUntil(this.clear$),
defaultIfEmpty([]),
),
),
);
for (let i = 0; i < items.length; i++) {
results.push(await items[i].decrypt(key));
}
const decryptedItems = (await Promise.all(results)).flat();
this.logService.info(
`Finished decrypting ${decryptedItems.length} items using ${numberOfWorkers} workers`,
);
this.restartTimeout();
return decryptedItems;
return results;
}
private updateWorkerServerConfigs(newConfig: ServerConfig) {
this.workers.forEach((worker) => {
const request = buildSetConfigMessage({ newConfig });
worker.postMessage(request);
});
}
private clear() {
this.clear$.next();
for (const worker of this.workers) {
worker.terminate();
}
this.workers = [];
this.clearTimeout();
}
private restartTimeout() {
this.clearTimeout();
this.timeout = setTimeout(() => this.clear(), workerTTL);
}
private clearTimeout() {
if (this.timeout != null) {
clearTimeout(this.timeout);
}
}
onServerConfigChange(newConfig: ServerConfig): void {}
}

View File

@@ -3,36 +3,20 @@
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
EncryptionType,
encryptionTypeToString as encryptionTypeName,
} from "@bitwarden/common/platform/enums";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
import {
Aes256CbcHmacKey,
Aes256CbcKey,
SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PureCrypto } from "@bitwarden/sdk-internal";
import {
DefaultFeatureFlagValue,
FeatureFlag,
getFeatureFlagValue,
} from "../../../enums/feature-flag.enum";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { EncryptService } from "../abstractions/encrypt.service";
export class EncryptServiceImplementation implements EncryptService {
protected useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
private blockType0: boolean = DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0];
constructor(
protected cryptoFunctionService: CryptoFunctionService,
protected logService: LogService,
@@ -41,27 +25,40 @@ export class EncryptServiceImplementation implements EncryptService {
// Proxy functions; Their implementation are temporary before moving at this level to the SDK
async encryptString(plainValue: string, key: SymmetricCryptoKey): Promise<EncString> {
return this.encrypt(plainValue, key);
if (plainValue == null) {
this.logService.warning(
"[EncryptService] WARNING: encryptString called with null value. Returning null, but this behavior is deprecated and will be removed.",
);
return null;
}
await SdkLoadService.Ready;
return new EncString(PureCrypto.symmetric_encrypt_string(plainValue, key.toEncoded()));
}
async encryptBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
return this.encrypt(plainValue, key);
await SdkLoadService.Ready;
return new EncString(PureCrypto.symmetric_encrypt_bytes(plainValue, key.toEncoded()));
}
async encryptFileData(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
return this.encryptToBytes(plainValue, key);
await SdkLoadService.Ready;
return new EncArrayBuffer(PureCrypto.symmetric_encrypt_filedata(plainValue, key.toEncoded()));
}
async decryptString(encString: EncString, key: SymmetricCryptoKey): Promise<string> {
return this.decryptToUtf8(encString, key);
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt_string(encString.encryptedString, key.toEncoded());
}
async decryptBytes(encString: EncString, key: SymmetricCryptoKey): Promise<Uint8Array> {
return this.decryptToBytes(encString, key);
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt_bytes(encString.encryptedString, key.toEncoded());
}
async decryptFileData(encBuffer: EncArrayBuffer, key: SymmetricCryptoKey): Promise<Uint8Array> {
return this.decryptToBytes(encBuffer, key);
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt_filedata(encBuffer.buffer, key.toEncoded());
}
async wrapDecapsulationKey(
@@ -76,7 +73,10 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No wrappingKey provided for wrapping.");
}
return await this.encryptUint8Array(decapsulationKeyPkcs8, wrappingKey);
await SdkLoadService.Ready;
return new EncString(
PureCrypto.wrap_decapsulation_key(decapsulationKeyPkcs8, wrappingKey.toEncoded()),
);
}
async wrapEncapsulationKey(
@@ -91,7 +91,10 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No wrappingKey provided for wrapping.");
}
return await this.encryptUint8Array(encapsulationKeySpki, wrappingKey);
await SdkLoadService.Ready;
return new EncString(
PureCrypto.wrap_encapsulation_key(encapsulationKeySpki, wrappingKey.toEncoded()),
);
}
async wrapSymmetricKey(
@@ -106,26 +109,61 @@ export class EncryptServiceImplementation implements EncryptService {
throw new Error("No wrappingKey provided for wrapping.");
}
return await this.encryptUint8Array(keyToBeWrapped.toEncoded(), wrappingKey);
await SdkLoadService.Ready;
return new EncString(
PureCrypto.wrap_symmetric_key(keyToBeWrapped.toEncoded(), wrappingKey.toEncoded()),
);
}
async unwrapDecapsulationKey(
wrappedDecapsulationKey: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<Uint8Array> {
return this.decryptBytes(wrappedDecapsulationKey, wrappingKey);
if (wrappedDecapsulationKey == null) {
throw new Error("No wrappedDecapsulationKey provided for unwrapping.");
}
if (wrappingKey == null) {
throw new Error("No wrappingKey provided for unwrapping.");
}
await SdkLoadService.Ready;
return PureCrypto.unwrap_decapsulation_key(
wrappedDecapsulationKey.encryptedString,
wrappingKey.toEncoded(),
);
}
async unwrapEncapsulationKey(
wrappedEncapsulationKey: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<Uint8Array> {
return this.decryptBytes(wrappedEncapsulationKey, wrappingKey);
if (wrappedEncapsulationKey == null) {
throw new Error("No wrappedEncapsulationKey provided for unwrapping.");
}
if (wrappingKey == null) {
throw new Error("No wrappingKey provided for unwrapping.");
}
await SdkLoadService.Ready;
return PureCrypto.unwrap_encapsulation_key(
wrappedEncapsulationKey.encryptedString,
wrappingKey.toEncoded(),
);
}
async unwrapSymmetricKey(
keyToBeUnwrapped: EncString,
wrappingKey: SymmetricCryptoKey,
): Promise<SymmetricCryptoKey> {
return new SymmetricCryptoKey(await this.decryptBytes(keyToBeUnwrapped, wrappingKey));
if (keyToBeUnwrapped == null) {
throw new Error("No keyToBeUnwrapped provided for unwrapping.");
}
if (wrappingKey == null) {
throw new Error("No wrappingKey provided for unwrapping.");
}
await SdkLoadService.Ready;
return new SymmetricCryptoKey(
PureCrypto.unwrap_symmetric_key(keyToBeUnwrapped.encryptedString, wrappingKey.toEncoded()),
);
}
async hash(value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512"): Promise<string> {
@@ -134,261 +172,33 @@ export class EncryptServiceImplementation implements EncryptService {
}
// Handle updating private properties to turn on/off feature flags.
onServerConfigChange(newConfig: ServerConfig): void {
const oldFlagValue = this.useSDKForDecryption;
this.useSDKForDecryption = getFeatureFlagValue(newConfig, FeatureFlag.UseSDKForDecryption);
this.logService.debug(
"[EncryptService] Updated sdk decryption flag",
oldFlagValue,
this.useSDKForDecryption,
);
this.blockType0 = getFeatureFlagValue(newConfig, FeatureFlag.PM17987_BlockType0);
}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64) {
throw new Error("Type 0 encryption is not supported.");
}
}
if (plainValue == null) {
return Promise.resolve(null);
}
if (typeof plainValue === "string") {
return this.encryptUint8Array(Utils.fromUtf8ToArray(plainValue), key);
} else {
return this.encryptUint8Array(plainValue, key);
}
}
private async encryptUint8Array(
plainValue: Uint8Array,
key: SymmetricCryptoKey,
): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64) {
throw new Error("Type 0 encryption is not supported.");
}
}
if (plainValue == null) {
return Promise.resolve(null);
}
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encObj = await this.aesEncrypt(plainValue, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = Utils.fromBufferToB64(encObj.mac);
return new EncString(innerKey.type, data, iv, mac);
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const encObj = await this.aesEncryptLegacy(plainValue, innerKey);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
return new EncString(innerKey.type, data, iv);
}
}
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (this.blockType0) {
if (key.inner().type === EncryptionType.AesCbc256_B64) {
throw new Error("Type 0 encryption is not supported.");
}
}
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const encValue = await this.aesEncrypt(plainValue, innerKey);
const macLen = encValue.mac.length;
const encBytes = new Uint8Array(
1 + encValue.iv.byteLength + macLen + encValue.data.byteLength,
);
encBytes.set([innerKey.type]);
encBytes.set(new Uint8Array(encValue.iv), 1);
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
return new EncArrayBuffer(encBytes);
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const encValue = await this.aesEncryptLegacy(plainValue, innerKey);
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + encValue.data.byteLength);
encBytes.set([innerKey.type]);
encBytes.set(new Uint8Array(encValue.iv), 1);
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength);
return new EncArrayBuffer(encBytes);
}
}
onServerConfigChange(newConfig: ServerConfig): void {}
async decryptToUtf8(
encString: EncString,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
_decryptContext: string = "no context",
): Promise<string> {
if (this.useSDKForDecryption) {
this.logService.debug("decrypting with SDK");
if (encString == null || encString.encryptedString == null) {
throw new Error("encString is null or undefined");
}
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.toEncoded());
}
this.logService.debug("decrypting with javascript");
if (key == null) {
throw new Error("No key provided for decryption.");
}
const innerKey = key.inner();
if (encString.encryptionType !== innerKey.type) {
this.logDecryptError(
"Key encryption type does not match payload encryption type",
innerKey.type,
encString.encryptionType,
decryptContext,
);
return null;
}
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
encString.mac,
key,
);
const computedMac = await this.cryptoFunctionService.hmacFast(
fastParams.macData,
fastParams.macKey,
"sha256",
);
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
if (!macsEqual) {
this.logMacFailed(
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
innerKey.type,
encString.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
encString.data,
encString.iv,
undefined,
key,
);
return await this.cryptoFunctionService.aesDecryptFast({
mode: "cbc",
parameters: fastParams,
});
} else {
throw new Error(`Unsupported encryption type`);
}
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.toEncoded());
}
async decryptToBytes(
encThing: Encrypted,
key: SymmetricCryptoKey,
decryptContext: string = "no context",
_decryptContext: string = "no context",
): Promise<Uint8Array | null> {
if (this.useSDKForDecryption) {
this.logService.debug("[EncryptService] Decrypting bytes with SDK");
if (
encThing.encryptionType == null ||
encThing.ivBytes == null ||
encThing.dataBytes == null
) {
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
}
const buffer = EncArrayBuffer.fromParts(
encThing.encryptionType,
encThing.ivBytes,
encThing.dataBytes,
encThing.macBytes,
).buffer;
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.toEncoded());
}
this.logService.debug("[EncryptService] Decrypting bytes with javascript");
if (key == null) {
throw new Error("No encryption key provided.");
}
if (encThing == null) {
throw new Error("Nothing provided for decryption.");
}
const inner = key.inner();
if (encThing.encryptionType !== inner.type) {
this.logDecryptError(
"Encryption key type mismatch",
inner.type,
encThing.encryptionType,
decryptContext,
);
return null;
}
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
if (encThing.macBytes == null) {
this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext);
return null;
}
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
macData.set(new Uint8Array(encThing.ivBytes), 0);
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(
macData,
inner.authenticationKey,
"sha256",
);
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"MAC comparison failed. Key or payload has changed.",
inner.type,
encThing.encryptionType,
decryptContext,
);
return null;
}
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
inner.encryptionKey,
"cbc",
);
} else if (inner.type === EncryptionType.AesCbc256_B64) {
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
inner.encryptionKey,
"cbc",
);
if (encThing.encryptionType == null || encThing.ivBytes == null || encThing.dataBytes == null) {
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
}
const buffer = EncArrayBuffer.fromParts(
encThing.encryptionType,
encThing.ivBytes,
encThing.dataBytes,
encThing.macBytes,
).buffer;
await SdkLoadService.Ready;
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.toEncoded());
}
async encapsulateKeyUnsigned(
@@ -398,14 +208,31 @@ export class EncryptServiceImplementation implements EncryptService {
if (sharedKey == null) {
throw new Error("No sharedKey provided for encapsulation");
}
return await this.rsaEncrypt(sharedKey.toEncoded(), encapsulationKey);
if (encapsulationKey == null) {
throw new Error("No encapsulationKey provided for encapsulation");
}
await SdkLoadService.Ready;
return new EncString(
PureCrypto.encapsulate_key_unsigned(sharedKey.toEncoded(), encapsulationKey),
);
}
async decapsulateKeyUnsigned(
encryptedSharedKey: EncString,
decapsulationKey: Uint8Array,
): Promise<SymmetricCryptoKey> {
const keyBytes = await this.rsaDecrypt(encryptedSharedKey, decapsulationKey);
if (encryptedSharedKey == null) {
throw new Error("No encryptedSharedKey provided for decapsulation");
}
if (decapsulationKey == null) {
throw new Error("No decapsulationKey provided for decapsulation");
}
const keyBytes = PureCrypto.decapsulate_key_unsigned(
encryptedSharedKey.encryptedString,
decapsulationKey,
);
await SdkLoadService.Ready;
return new SymmetricCryptoKey(keyBytes);
}
@@ -428,51 +255,6 @@ export class EncryptServiceImplementation implements EncryptService {
return results;
}
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
macData.set(new Uint8Array(obj.iv), 0);
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
obj.mac = await this.cryptoFunctionService.hmac(macData, key.authenticationKey, "sha256");
return obj;
}
/**
* @deprecated Removed once AesCbc256_B64 support is removed
*/
private async aesEncryptLegacy(data: Uint8Array, key: Aes256CbcKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
return obj;
}
private logDecryptError(
msg: string,
keyEncType: EncryptionType,
dataEncType: EncryptionType,
decryptContext: string,
) {
this.logService.error(
`[Encrypt service] ${msg} Key type ${encryptionTypeName(keyEncType)} Payload type ${encryptionTypeName(dataEncType)} Decrypt context: ${decryptContext}`,
);
}
private logMacFailed(
msg: string,
keyEncType: EncryptionType,
dataEncType: EncryptionType,
decryptContext: string,
) {
if (this.logMacFailures) {
this.logDecryptError(msg, keyEncType, dataEncType, decryptContext);
}
}
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
if (data == null) {
throw new Error("No data provided for encryption.");

View File

@@ -3,20 +3,14 @@ import { mockReset, mock } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import {
Aes256CbcHmacKey,
SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { makeStaticByteArray } from "../../../../spec";
import { DefaultFeatureFlagValue, FeatureFlag } from "../../../enums/feature-flag.enum";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { SdkLoadService } from "../../../platform/abstractions/sdk/sdk-load.service";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
@@ -26,24 +20,50 @@ describe("EncryptService", () => {
let encryptService: EncryptServiceImplementation;
const testEncBuffer = EncArrayBuffer.fromParts(
EncryptionType.AesCbc256_HmacSha256_B64,
new Uint8Array(16),
new Uint8Array(32),
new Uint8Array(32),
);
beforeEach(() => {
mockReset(cryptoFunctionService);
mockReset(logService);
jest.spyOn(PureCrypto, "symmetric_decrypt_array_buffer").mockReturnValue(new Uint8Array(1));
jest.spyOn(PureCrypto, "symmetric_decrypt").mockReturnValue("decrypted_string");
jest.spyOn(PureCrypto, "symmetric_decrypt_filedata").mockReturnValue(new Uint8Array(1));
jest.spyOn(PureCrypto, "symmetric_encrypt_filedata").mockReturnValue(testEncBuffer.buffer);
jest.spyOn(PureCrypto, "symmetric_decrypt_string").mockReturnValue("decrypted_string");
jest.spyOn(PureCrypto, "symmetric_encrypt_string").mockReturnValue("encrypted_string");
jest.spyOn(PureCrypto, "symmetric_decrypt_bytes").mockReturnValue(new Uint8Array(3));
jest.spyOn(PureCrypto, "symmetric_encrypt_bytes").mockReturnValue("encrypted_bytes");
jest.spyOn(PureCrypto, "wrap_decapsulation_key").mockReturnValue("wrapped_decapsulation_key");
jest.spyOn(PureCrypto, "wrap_encapsulation_key").mockReturnValue("wrapped_encapsulation_key");
jest.spyOn(PureCrypto, "wrap_symmetric_key").mockReturnValue("wrapped_symmetric_key");
jest.spyOn(PureCrypto, "unwrap_decapsulation_key").mockReturnValue(new Uint8Array(4));
jest.spyOn(PureCrypto, "unwrap_encapsulation_key").mockReturnValue(new Uint8Array(5));
jest.spyOn(PureCrypto, "unwrap_symmetric_key").mockReturnValue(new Uint8Array(64));
jest.spyOn(PureCrypto, "decapsulate_key_unsigned").mockReturnValue(new Uint8Array(64));
jest.spyOn(PureCrypto, "encapsulate_key_unsigned").mockReturnValue("encapsulated_key_unsigned");
(SdkLoadService as any).Ready = jest.fn().mockResolvedValue(true);
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
});
describe("wrapSymmetricKey", () => {
it("roundtrip encrypts and decrypts a symmetric key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapSymmetricKey(key, wrappingKey);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
await encryptService.wrapSymmetricKey(key, wrappingKey);
expect(PureCrypto.wrap_symmetric_key).toHaveBeenCalledWith(
key.toEncoded(),
wrappingKey.toEncoded(),
);
});
it("fails if key toBeWrapped is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
@@ -57,33 +77,17 @@ describe("EncryptService", () => {
"No wrappingKey provided for wrapping.",
);
});
it("fails if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
});
describe("wrapDecapsulationKey", () => {
it("roundtrip encrypts and decrypts a decapsulation key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
it("is a proxy to PureCrypto", async () => {
const decapsulationKey = makeStaticByteArray(10);
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapDecapsulationKey(
makeStaticByteArray(64),
wrappingKey,
await encryptService.wrapDecapsulationKey(decapsulationKey, wrappingKey);
expect(PureCrypto.wrap_decapsulation_key).toHaveBeenCalledWith(
decapsulationKey,
wrappingKey.toEncoded(),
);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if decapsulation key is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
@@ -97,33 +101,17 @@ describe("EncryptService", () => {
"No wrappingKey provided for wrapping.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(
encryptService.wrapDecapsulationKey(new Uint8Array(200), mock32Key),
).rejects.toThrow("Type 0 encryption is not supported.");
});
});
describe("wrapEncapsulationKey", () => {
it("roundtrip encrypts and decrypts an encapsulationKey key", async () => {
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(64, 0));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
it("is a proxy to PureCrypto", async () => {
const encapsulationKey = makeStaticByteArray(10);
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = await encryptService.wrapEncapsulationKey(
makeStaticByteArray(64),
wrappingKey,
await encryptService.wrapEncapsulationKey(encapsulationKey, wrappingKey);
expect(PureCrypto.wrap_encapsulation_key).toHaveBeenCalledWith(
encapsulationKey,
wrappingKey.toEncoded(),
);
expect(encString.encryptionType).toEqual(EncryptionType.AesCbc256_HmacSha256_B64);
expect(encString.data).toEqual(Utils.fromBufferToB64(makeStaticByteArray(64, 0)));
});
it("fails if encapsulation key is null", async () => {
const wrappingKey = new SymmetricCryptoKey(makeStaticByteArray(64));
@@ -137,535 +125,152 @@ describe("EncryptService", () => {
"No wrappingKey provided for wrapping.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(
encryptService.wrapEncapsulationKey(new Uint8Array(200), mock32Key),
).rejects.toThrow("Type 0 encryption is not supported.");
});
});
describe("onServerConfigChange", () => {
const newConfig = mock<ServerConfig>();
afterEach(() => {
jest.resetAllMocks();
});
it("updates internal flag with default value when not present in config", () => {
encryptService.onServerConfigChange(newConfig);
expect((encryptService as any).blockType0).toBe(
DefaultFeatureFlagValue[FeatureFlag.PM17987_BlockType0],
);
});
test.each([true, false])("updates internal flag with value in config", (expectedValue) => {
newConfig.featureStates = { [FeatureFlag.PM17987_BlockType0]: expectedValue };
encryptService.onServerConfigChange(newConfig);
expect((encryptService as any).blockType0).toBe(expectedValue);
});
});
describe("encrypt", () => {
it("throws if no key is provided", () => {
return expect(encryptService.encrypt(null, null)).rejects.toThrow(
"No encryption key provided.",
);
});
it("throws if type 0 key is provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(encryptService.encrypt(null!, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encrypt(null!, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
const plainValue = "data";
await expect(encryptService.encrypt(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encrypt(plainValue, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
it("returns null if no data is provided with valid key", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const actual = await encryptService.encrypt(null, key);
expect(actual).toBeNull();
});
it("creates an EncString for Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const plainValue = "data";
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(4, 100));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
const result = await encryptService.encrypt(plainValue, key);
expect(cryptoFunctionService.aesEncrypt).toHaveBeenCalledWith(
Utils.fromByteStringToArray(plainValue),
makeStaticByteArray(16),
makeStaticByteArray(32),
);
expect(cryptoFunctionService.hmac).not.toHaveBeenCalled();
expect(Utils.fromB64ToArray(result.data).length).toEqual(4);
expect(Utils.fromB64ToArray(result.iv).length).toEqual(16);
});
it("creates an EncString for Aes256Cbc_HmacSha256_B64", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data";
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(32));
cryptoFunctionService.aesEncrypt.mockResolvedValue(makeStaticByteArray(4, 100));
cryptoFunctionService.randomBytes.mockResolvedValue(makeStaticByteArray(16) as CsprngArray);
const result = await encryptService.encrypt(plainValue, key);
expect(cryptoFunctionService.aesEncrypt).toHaveBeenCalledWith(
Utils.fromByteStringToArray(plainValue),
makeStaticByteArray(16),
makeStaticByteArray(32),
);
const macData = new Uint8Array(16 + 4);
macData.set(makeStaticByteArray(16));
macData.set(makeStaticByteArray(4, 100), 16);
expect(cryptoFunctionService.hmac).toHaveBeenCalledWith(
macData,
makeStaticByteArray(32, 32),
"sha256",
);
expect(Utils.fromB64ToArray(result.data).length).toEqual(4);
expect(Utils.fromB64ToArray(result.iv).length).toEqual(16);
expect(Utils.fromB64ToArray(result.mac).length).toEqual(32);
});
});
describe("encryptToBytes", () => {
const plainValue = makeStaticByteArray(16, 1);
it("throws if no key is provided", () => {
return expect(encryptService.encryptToBytes(plainValue, null)).rejects.toThrow(
"No encryption key",
);
});
it("throws if type 0 key provided with flag turned on", async () => {
(encryptService as any).blockType0 = true;
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: makeStaticByteArray(32),
});
await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
await expect(encryptService.encryptToBytes(plainValue, mock32Key)).rejects.toThrow(
"Type 0 encryption is not supported.",
);
});
it("encrypts data with provided Aes256Cbc key and returns correct encbuffer", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const iv = makeStaticByteArray(16, 80);
const cipherText = makeStaticByteArray(20, 150);
cryptoFunctionService.randomBytes.mockResolvedValue(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
const actual = await encryptService.encryptToBytes(plainValue, key);
const expectedBytes = new Uint8Array(1 + iv.byteLength + cipherText.byteLength);
expectedBytes.set([EncryptionType.AesCbc256_B64]);
expectedBytes.set(iv, 1);
expectedBytes.set(cipherText, 1 + iv.byteLength);
expect(actual.buffer).toEqualBuffer(expectedBytes);
});
it("encrypts data with provided Aes256Cbc_HmacSha256 key and returns correct encbuffer", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const iv = makeStaticByteArray(16, 80);
const mac = makeStaticByteArray(32, 100);
const cipherText = makeStaticByteArray(20, 150);
cryptoFunctionService.randomBytes.mockResolvedValue(iv as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(cipherText);
cryptoFunctionService.hmac.mockResolvedValue(mac);
const actual = await encryptService.encryptToBytes(plainValue, key);
const expectedBytes = new Uint8Array(
1 + iv.byteLength + mac.byteLength + cipherText.byteLength,
);
expectedBytes.set([EncryptionType.AesCbc256_HmacSha256_B64]);
expectedBytes.set(iv, 1);
expectedBytes.set(mac, 1 + iv.byteLength);
expectedBytes.set(cipherText, 1 + iv.byteLength + mac.byteLength);
expect(actual.buffer).toEqualBuffer(expectedBytes);
});
});
describe("decryptToBytes", () => {
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100));
const computedMac = new Uint8Array(1);
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, encType));
beforeEach(() => {
cryptoFunctionService.hmac.mockResolvedValue(computedMac);
});
it("throws if no key is provided", () => {
return expect(encryptService.decryptToBytes(encBuffer, null)).rejects.toThrow(
"No encryption key",
);
});
it("throws if no encrypted value is provided", () => {
return expect(encryptService.decryptToBytes(null, key)).rejects.toThrow(
"Nothing provided for decryption",
);
});
it("calls PureCrypto when useSDKForDecryption is true", async () => {
(encryptService as any).useSDKForDecryption = true;
const decryptedBytes = makeStaticByteArray(10, 200);
Object.defineProperty(SdkLoadService, "Ready", {
value: Promise.resolve(),
configurable: true,
});
jest.spyOn(PureCrypto, "symmetric_decrypt_array_buffer").mockReturnValue(decryptedBytes);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(PureCrypto.symmetric_decrypt_array_buffer).toHaveBeenCalledWith(
encBuffer.buffer,
key.toEncoded(),
);
expect(actual).toEqualBuffer(decryptedBytes);
});
it("decrypts data with provided key for Aes256CbcHmac", async () => {
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
cryptoFunctionService.compare.mockResolvedValue(true);
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
expect(actual).toEqualBuffer(decryptedBytes);
});
it("decrypts data with provided key for Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encBuffer = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
const decryptedBytes = makeStaticByteArray(10, 200);
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
cryptoFunctionService.compare.mockResolvedValue(true);
cryptoFunctionService.aesDecrypt.mockResolvedValueOnce(decryptedBytes);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
expect(actual).toEqualBuffer(decryptedBytes);
});
it("compares macs using CryptoFunctionService", async () => {
const expectedMacData = new Uint8Array(
encBuffer.ivBytes.byteLength + encBuffer.dataBytes.byteLength,
);
expectedMacData.set(new Uint8Array(encBuffer.ivBytes));
expectedMacData.set(new Uint8Array(encBuffer.dataBytes), encBuffer.ivBytes.byteLength);
await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.hmac).toBeCalledWith(
expect.toEqualBuffer(expectedMacData),
(key.inner() as Aes256CbcHmacKey).authenticationKey,
"sha256",
);
expect(cryptoFunctionService.compare).toBeCalledWith(
expect.toEqualBuffer(encBuffer.macBytes),
expect.toEqualBuffer(computedMac),
);
});
it("returns null if macs don't match", async () => {
cryptoFunctionService.compare.mockResolvedValue(false);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.compare).toHaveBeenCalled();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
expect(actual).toBeNull();
});
it("returns null if mac could not be calculated", async () => {
cryptoFunctionService.hmac.mockResolvedValue(null);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(cryptoFunctionService.hmac).toHaveBeenCalled();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
expect(actual).toBeNull();
});
it("returns null if key is Aes256Cbc but encbuffer is Aes256Cbc_HmacSha256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
cryptoFunctionService.compare.mockResolvedValue(true);
const actual = await encryptService.decryptToBytes(encBuffer, key);
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
it("returns null if key is Aes256Cbc_HmacSha256 but encbuffer is Aes256Cbc", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
cryptoFunctionService.compare.mockResolvedValue(true);
const buffer = new EncArrayBuffer(makeStaticByteArray(200, EncryptionType.AesCbc256_B64));
const actual = await encryptService.decryptToBytes(buffer, key);
expect(actual).toBeNull();
expect(cryptoFunctionService.aesDecrypt).not.toHaveBeenCalled();
});
});
describe("decryptToUtf8", () => {
it("throws if no key is provided", () => {
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
"No key provided for decryption.",
);
});
it("calls PureCrypto when useSDKForDecryption is true", async () => {
(encryptService as any).useSDKForDecryption = true;
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
Object.defineProperty(SdkLoadService, "Ready", {
value: Promise.resolve(),
configurable: true,
});
jest.spyOn(PureCrypto, "symmetric_decrypt").mockReturnValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(PureCrypto.symmetric_decrypt).toHaveBeenCalledWith(
encString.encryptedString,
key.toEncoded(),
);
});
it("decrypts data with provided key for AesCbc256_HmacSha256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(true);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(cryptoFunctionService.compareFast).toHaveBeenCalledWith(
makeStaticByteArray(32, 0),
makeStaticByteArray(32, 0),
);
});
it("decrypts data with provided key for AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(true);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toEqual("data");
expect(cryptoFunctionService.compareFast).not.toHaveBeenCalled();
});
it("returns null if key is AesCbc256_HMAC but encstring is AesCbc256", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
it("returns null if key is AesCbc256 but encstring is AesCbc256_HMAC", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
it("returns null if macs don't match", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
macData: makeStaticByteArray(32, 0),
macKey: makeStaticByteArray(32, 0),
mac: makeStaticByteArray(32, 0),
} as any);
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
cryptoFunctionService.compareFast.mockResolvedValue(false);
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
});
});
describe("decryptToUtf8", () => {
it("throws if no key is provided", () => {
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
"No key provided for decryption.",
);
});
it("returns null if key is mac key but encstring has no mac", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const actual = await encryptService.decryptToUtf8(encString, key);
expect(actual).toBeNull();
expect(logService.error).toHaveBeenCalled();
});
});
describe("encryptString", () => {
it("is a proxy to encrypt", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = "data";
encryptService.encrypt = jest.fn();
await encryptService.encryptString(plainValue, key);
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
const result = await encryptService.encryptString(plainValue, key);
expect(result).toEqual(new EncString("encrypted_string"));
expect(PureCrypto.symmetric_encrypt_string).toHaveBeenCalledWith(plainValue, key.toEncoded());
});
});
describe("encryptBytes", () => {
it("is a proxy to encrypt", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = makeStaticByteArray(16, 1);
encryptService.encrypt = jest.fn();
await encryptService.encryptBytes(plainValue, key);
expect(encryptService.encrypt).toHaveBeenCalledWith(plainValue, key);
const result = await encryptService.encryptBytes(plainValue, key);
expect(result).toEqual(new EncString("encrypted_bytes"));
expect(PureCrypto.symmetric_encrypt_bytes).toHaveBeenCalledWith(plainValue, key.toEncoded());
});
});
describe("encryptFileData", () => {
it("is a proxy to encryptToBytes", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const plainValue = makeStaticByteArray(16, 1);
encryptService.encryptToBytes = jest.fn();
await encryptService.encryptFileData(plainValue, key);
expect(encryptService.encryptToBytes).toHaveBeenCalledWith(plainValue, key);
const result = await encryptService.encryptFileData(plainValue, key);
expect(result).toEqual(testEncBuffer);
expect(PureCrypto.symmetric_encrypt_filedata).toHaveBeenCalledWith(
plainValue,
key.toEncoded(),
);
});
});
describe("decryptString", () => {
it("is a proxy to decryptToUtf8", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptToUtf8 = jest.fn();
await encryptService.decryptString(encString, key);
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key);
const encString = new EncString("encrypted_string");
const result = await encryptService.decryptString(encString, key);
expect(result).toEqual("decrypted_string");
expect(PureCrypto.symmetric_decrypt_string).toHaveBeenCalledWith(
encString.encryptedString,
key.toEncoded(),
);
});
});
describe("decryptBytes", () => {
it("is a proxy to decryptToBytes", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptToBytes = jest.fn();
await encryptService.decryptBytes(encString, key);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
const encString = new EncString("encrypted_bytes");
const result = await encryptService.decryptBytes(encString, key);
expect(result).toEqual(new Uint8Array(3));
expect(PureCrypto.symmetric_decrypt_bytes).toHaveBeenCalledWith(
encString.encryptedString,
key.toEncoded(),
);
});
});
describe("decryptFileData", () => {
it("is a proxy to decrypt", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncArrayBuffer(makeStaticByteArray(60, EncryptionType.AesCbc256_B64));
encryptService.decryptToBytes = jest.fn();
await encryptService.decryptFileData(encString, key);
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(encString, key);
const encString = new EncArrayBuffer(testEncBuffer.buffer);
const result = await encryptService.decryptFileData(encString, key);
expect(result).toEqual(new Uint8Array(1));
expect(PureCrypto.symmetric_decrypt_filedata).toHaveBeenCalledWith(
encString.buffer,
key.toEncoded(),
);
});
});
describe("unwrapDecapsulationKey", () => {
it("is a proxy to decryptBytes", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapDecapsulationKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
const encString = new EncString("wrapped_decapsulation_key");
const result = await encryptService.unwrapDecapsulationKey(encString, key);
expect(result).toEqual(new Uint8Array(4));
expect(PureCrypto.unwrap_decapsulation_key).toHaveBeenCalledWith(
encString.encryptedString,
key.toEncoded(),
);
});
it("throws if wrappedDecapsulationKey is null", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
return expect(encryptService.unwrapDecapsulationKey(null, key)).rejects.toThrow(
"No wrappedDecapsulationKey provided for unwrapping.",
);
});
it("throws if wrappingKey is null", () => {
const encString = new EncString("wrapped_decapsulation_key");
return expect(encryptService.unwrapDecapsulationKey(encString, null)).rejects.toThrow(
"No wrappingKey provided for unwrapping.",
);
});
});
describe("unwrapEncapsulationKey", () => {
it("is a proxy to decryptBytes", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
encryptService.decryptBytes = jest.fn();
await encryptService.unwrapEncapsulationKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
const encString = new EncString("wrapped_encapsulation_key");
const result = await encryptService.unwrapEncapsulationKey(encString, key);
expect(result).toEqual(new Uint8Array(5));
expect(PureCrypto.unwrap_encapsulation_key).toHaveBeenCalledWith(
encString.encryptedString,
key.toEncoded(),
);
});
it("throws if wrappedEncapsulationKey is null", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
return expect(encryptService.unwrapEncapsulationKey(null, key)).rejects.toThrow(
"No wrappedEncapsulationKey provided for unwrapping.",
);
});
it("throws if wrappingKey is null", () => {
const encString = new EncString("wrapped_encapsulation_key");
return expect(encryptService.unwrapEncapsulationKey(encString, null)).rejects.toThrow(
"No wrappingKey provided for unwrapping.",
);
});
});
describe("unwrapSymmetricKey", () => {
it("is a proxy to decryptBytes", async () => {
it("is a proxy to PureCrypto", async () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
const jestFn = jest.fn();
jestFn.mockResolvedValue(new Uint8Array(64));
encryptService.decryptBytes = jestFn;
await encryptService.unwrapSymmetricKey(encString, key);
expect(encryptService.decryptBytes).toHaveBeenCalledWith(encString, key);
const encString = new EncString("wrapped_symmetric_key");
const result = await encryptService.unwrapSymmetricKey(encString, key);
expect(result).toEqual(new SymmetricCryptoKey(new Uint8Array(64)));
expect(PureCrypto.unwrap_symmetric_key).toHaveBeenCalledWith(
encString.encryptedString,
key.toEncoded(),
);
});
it("throws if keyToBeUnwrapped is null", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
return expect(encryptService.unwrapSymmetricKey(null, key)).rejects.toThrow(
"No keyToBeUnwrapped provided for unwrapping.",
);
});
it("throws if wrappingKey is null", () => {
const encString = new EncString("wrapped_symmetric_key");
return expect(encryptService.unwrapSymmetricKey(encString, null)).rejects.toThrow(
"No wrappingKey provided for unwrapping.",
);
});
});
@@ -690,23 +295,13 @@ describe("EncryptService", () => {
it("throws if no public key is provided", () => {
return expect(encryptService.encapsulateKeyUnsigned(testKey, null)).rejects.toThrow(
"No public key",
"No encapsulationKey provided for encapsulation",
);
});
it("encrypts data with provided key", async () => {
cryptoFunctionService.rsaEncrypt.mockResolvedValue(encryptedData);
const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey);
expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith(
expect.toEqualBuffer(testKey.toEncoded()),
expect.toEqualBuffer(publicKey),
"sha1",
);
expect(actual).toEqual(encString);
expect(actual.dataBytes).toEqualBuffer(encryptedData);
expect(actual).toEqual(new EncString("encapsulated_key_unsigned"));
});
it("throws if no data was provided", () => {
@@ -719,39 +314,19 @@ describe("EncryptService", () => {
describe("decapsulateKeyUnsigned", () => {
it("throws if no data is provided", () => {
return expect(encryptService.decapsulateKeyUnsigned(null, privateKey)).rejects.toThrow(
"No data",
"No encryptedSharedKey provided for decapsulation",
);
});
it("throws if no private key is provided", () => {
return expect(encryptService.decapsulateKeyUnsigned(encString, null)).rejects.toThrow(
"No private key",
"No decapsulationKey provided for decapsulation",
);
});
it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])(
"throws if encryption type is %s",
async (encType) => {
encString.encryptionType = encType;
await expect(
encryptService.decapsulateKeyUnsigned(encString, privateKey),
).rejects.toThrow("Invalid encryption type");
},
);
it("decrypts data with provided key", async () => {
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
const actual = await encryptService.decapsulateKeyUnsigned(makeEncString(data), privateKey);
expect(cryptoFunctionService.rsaDecrypt).toBeCalledWith(
expect.toEqualBuffer(data),
expect.toEqualBuffer(privateKey),
"sha1",
);
expect(actual.toEncoded()).toEqualBuffer(data);
expect(actual.toEncoded()).toEqualBuffer(new Uint8Array(64));
});
});
});

View File

@@ -1,97 +0,0 @@
import { mock } from "jest-mock-extended";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { BulkEncryptService } from "../abstractions/bulk-encrypt.service";
import { EncryptService } from "../abstractions/encrypt.service";
import { FallbackBulkEncryptService } from "./fallback-bulk-encrypt.service";
describe("FallbackBulkEncryptService", () => {
const encryptService = mock<EncryptService>();
const featureFlagEncryptService = mock<BulkEncryptService>();
const serverConfig = mock<ServerConfig>();
let sut: FallbackBulkEncryptService;
beforeEach(() => {
sut = new FallbackBulkEncryptService(encryptService);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("decryptItems", () => {
const key = mock<SymmetricCryptoKey>();
const mockItems = [{ id: "guid", name: "encryptedValue" }] as any[];
const mockDecryptedItems = [{ id: "guid", name: "decryptedValue" }] as any[];
it("calls decryptItems on featureFlagEncryptService when it is set", async () => {
featureFlagEncryptService.decryptItems.mockResolvedValue(mockDecryptedItems);
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
const result = await sut.decryptItems(mockItems, key);
expect(featureFlagEncryptService.decryptItems).toHaveBeenCalledWith(mockItems, key);
expect(encryptService.decryptItems).not.toHaveBeenCalled();
expect(result).toEqual(mockDecryptedItems);
});
it("calls decryptItems on encryptService when featureFlagEncryptService is not set", async () => {
encryptService.decryptItems.mockResolvedValue(mockDecryptedItems);
const result = await sut.decryptItems(mockItems, key);
expect(encryptService.decryptItems).toHaveBeenCalledWith(mockItems, key);
expect(result).toEqual(mockDecryptedItems);
});
});
describe("setFeatureFlagEncryptService", () => {
it("sets the featureFlagEncryptService property", async () => {
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
});
it("does not call onServerConfigChange when currentServerConfig is undefined", async () => {
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
expect(featureFlagEncryptService.onServerConfigChange).not.toHaveBeenCalled();
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
});
it("calls onServerConfigChange with currentServerConfig when it is defined", async () => {
sut.onServerConfigChange(serverConfig);
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
expect(featureFlagEncryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
expect((sut as any).featureFlagEncryptService).toBe(featureFlagEncryptService);
});
});
describe("onServerConfigChange", () => {
it("updates internal currentServerConfig to new config", async () => {
sut.onServerConfigChange(serverConfig);
expect((sut as any).currentServerConfig).toBe(serverConfig);
});
it("calls onServerConfigChange on featureFlagEncryptService when it is set", async () => {
await sut.setFeatureFlagEncryptService(featureFlagEncryptService);
sut.onServerConfigChange(serverConfig);
expect(featureFlagEncryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
expect(encryptService.onServerConfigChange).not.toHaveBeenCalled();
});
it("calls onServerConfigChange on encryptService when featureFlagEncryptService is not set", () => {
sut.onServerConfigChange(serverConfig);
expect(encryptService.onServerConfigChange).toHaveBeenCalledWith(serverConfig);
});
});
});

View File

@@ -9,7 +9,7 @@ import { ServerConfig } from "../../../platform/abstractions/config/server-confi
import { EncryptService } from "../abstractions/encrypt.service";
/**
* @deprecated For the feature flag from PM-4154, remove once feature is rolled out
* @deprecated Will be deleted in an immediate subsequent PR
*/
export class FallbackBulkEncryptService implements BulkEncryptService {
private featureFlagEncryptService: BulkEncryptService;
@@ -25,22 +25,10 @@ export class FallbackBulkEncryptService implements BulkEncryptService {
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (this.featureFlagEncryptService != null) {
return await this.featureFlagEncryptService.decryptItems(items, key);
} else {
return await this.encryptService.decryptItems(items, key);
}
return await this.encryptService.decryptItems(items, key);
}
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {
if (this.currentServerConfig !== undefined) {
featureFlagEncryptService.onServerConfigChange(this.currentServerConfig);
}
this.featureFlagEncryptService = featureFlagEncryptService;
}
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {}
onServerConfigChange(newConfig: ServerConfig): void {
this.currentServerConfig = newConfig;
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
}
onServerConfigChange(newConfig: ServerConfig): void {}
}

View File

@@ -1,123 +0,0 @@
import { mock } from "jest-mock-extended";
import * as rxjs from "rxjs";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { LogService } from "../../../platform/abstractions/log.service";
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { buildSetConfigMessage } from "../types/worker-command.type";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
import { MultithreadEncryptServiceImplementation } from "./multithread-encrypt.service.implementation";
describe("MultithreadEncryptServiceImplementation", () => {
const cryptoFunctionService = mock<CryptoFunctionService>();
const logService = mock<LogService>();
const serverConfig = mock<ServerConfig>();
let sut: MultithreadEncryptServiceImplementation;
beforeEach(() => {
sut = new MultithreadEncryptServiceImplementation(cryptoFunctionService, logService, true);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("decryptItems", () => {
const key = mock<SymmetricCryptoKey>();
const mockWorker = mock<Worker>();
beforeEach(() => {
// Mock creating a worker.
global.Worker = jest.fn().mockImplementation(() => mockWorker);
global.URL = jest.fn().mockImplementation(() => "url") as unknown as typeof URL;
global.URL.createObjectURL = jest.fn().mockReturnValue("blob:url");
global.URL.revokeObjectURL = jest.fn();
global.URL.canParse = jest.fn().mockReturnValue(true);
// Mock the workers returned response.
const mockMessageEvent = {
id: "mock-guid",
data: ["decrypted1", "decrypted2"],
};
const mockMessageEvent$ = rxjs.from([mockMessageEvent]);
jest.spyOn(rxjs, "fromEvent").mockReturnValue(mockMessageEvent$);
});
it("returns empty array if items is null", async () => {
const items = null as unknown as Decryptable<any>[];
const result = await sut.decryptItems(items, key);
expect(result).toEqual([]);
});
it("returns empty array if items is empty", async () => {
const result = await sut.decryptItems([], key);
expect(result).toEqual([]);
});
it("creates worker if none exists", async () => {
// Make sure currentServerConfig is undefined so a SetConfigMessage is not sent.
(sut as any).currentServerConfig = undefined;
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
expect(global.Worker).toHaveBeenCalled();
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
it("sends a SetConfigMessage to the new worker when there is a current server config", async () => {
// Populate currentServerConfig so a SetConfigMessage is sent.
(sut as any).currentServerConfig = serverConfig;
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
expect(global.Worker).toHaveBeenCalled();
expect(mockWorker.postMessage).toHaveBeenCalledTimes(2);
expect(mockWorker.postMessage).toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
it("does not create worker if one exists", async () => {
(sut as any).currentServerConfig = serverConfig;
(sut as any).worker = mockWorker;
await sut.decryptItems([mock<Decryptable<any>>(), mock<Decryptable<any>>()], key);
expect(global.Worker).not.toHaveBeenCalled();
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
expect(mockWorker.postMessage).not.toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
});
describe("onServerConfigChange", () => {
it("updates internal currentServerConfig to new config and calls super", () => {
const superSpy = jest.spyOn(EncryptServiceImplementation.prototype, "onServerConfigChange");
sut.onServerConfigChange(serverConfig);
expect(superSpy).toHaveBeenCalledWith(serverConfig);
expect((sut as any).currentServerConfig).toBe(serverConfig);
});
it("sends config update to worker if worker exists", () => {
const mockWorker = mock<Worker>();
(sut as any).worker = mockWorker;
sut.onServerConfigChange(serverConfig);
expect(mockWorker.postMessage).toHaveBeenCalledTimes(1);
expect(mockWorker.postMessage).toHaveBeenCalledWith(
buildSetConfigMessage({ newConfig: serverConfig }),
);
});
});
});

View File

@@ -1,31 +1,16 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs";
import { Jsonify } from "type-fest";
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
import { buildDecryptMessage, buildSetConfigMessage } from "../types/worker-command.type";
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
const workerTTL = 3 * 60000; // 3 minutes
/**
* @deprecated Replaced by BulkEncryptionService (PM-4154)
* @deprecated Will be deleted in an immediate subsequent PR
*/
export class MultithreadEncryptServiceImplementation extends EncryptServiceImplementation {
private worker: Worker;
private timeout: any;
private currentServerConfig: ServerConfig | undefined = undefined;
private clear$ = new Subject<void>();
protected useSDKForDecryption: boolean = true;
/**
* Sends items to a web worker to decrypt them.
@@ -35,84 +20,8 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
items: Decryptable<T>[],
key: SymmetricCryptoKey,
): Promise<T[]> {
if (items == null || items.length < 1) {
return [];
}
if (this.useSDKForDecryption) {
return await super.decryptItems(items, key);
}
this.logService.info("Starting decryption using multithreading");
if (this.worker == null) {
this.worker = new Worker(
new URL(
/* webpackChunkName: 'encrypt-worker' */
"@bitwarden/common/key-management/crypto/services/encrypt.worker.ts",
import.meta.url,
),
);
if (this.currentServerConfig !== undefined) {
this.updateWorkerServerConfig(this.currentServerConfig);
}
}
this.restartTimeout();
const id = Utils.newGuid();
const request = buildDecryptMessage({
id,
items: items,
key: key,
});
this.worker.postMessage(request);
return await firstValueFrom(
fromEvent(this.worker, "message").pipe(
filter((response: MessageEvent) => response.data?.id === id),
map((response) => JSON.parse(response.data.items)),
map((items) =>
items.map((jsonItem: Jsonify<T>) => {
const initializer = getClassInitializer<T>(jsonItem.initializerKey);
return initializer(jsonItem);
}),
),
takeUntil(this.clear$),
defaultIfEmpty([]),
),
);
return await super.decryptItems(items, key);
}
override onServerConfigChange(newConfig: ServerConfig): void {
this.currentServerConfig = newConfig;
super.onServerConfigChange(newConfig);
this.updateWorkerServerConfig(newConfig);
}
private updateWorkerServerConfig(newConfig: ServerConfig) {
if (this.worker != null) {
const request = buildSetConfigMessage({ newConfig });
this.worker.postMessage(request);
}
}
private clear() {
this.clear$.next();
this.worker?.terminate();
this.worker = null;
this.clearTimeout();
}
private restartTimeout() {
this.clearTimeout();
this.timeout = setTimeout(() => this.clear(), workerTTL);
}
private clearTimeout() {
if (this.timeout != null) {
clearTimeout(this.timeout);
}
}
override onServerConfigChange(newConfig: ServerConfig): void {}
}

View File

@@ -233,48 +233,6 @@ describe("WebCrypto Function Service", () => {
});
});
describe("aesEncrypt CBC mode", () => {
it("should successfully encrypt data", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const data = Utils.fromUtf8ToArray("EncryptMe!");
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA==");
});
it("should successfully encrypt and then decrypt data fast", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = await cryptoFunctionService.aesEncrypt(data, iv, key);
const encData = Utils.fromBufferToB64(encValue);
const b64Iv = Utils.fromBufferToB64(iv);
const symKey = new SymmetricCryptoKey(key);
const parameters = cryptoFunctionService.aesDecryptFastParameters(
encData,
b64Iv,
null,
symKey,
);
const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters });
expect(decValue).toBe(value);
});
it("should successfully encrypt and then decrypt data", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();
const iv = makeStaticByteArray(16);
const key = makeStaticByteArray(32);
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = new Uint8Array(await cryptoFunctionService.aesEncrypt(data, iv, key));
const decValue = await cryptoFunctionService.aesDecrypt(encValue, iv, key, "cbc");
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
});
});
describe("aesDecryptFast CBC mode", () => {
it("should successfully decrypt data", async () => {
const cryptoFunctionService = getWebCryptoFunctionService();

View File

@@ -204,14 +204,6 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
return equals;
}
async aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [
"encrypt",
]);
const buffer = await this.subtle.encrypt({ name: "AES-CBC", iv: iv }, impKey, data);
return new Uint8Array(buffer);
}
aesDecryptFastParameters(
data: string,
iv: string,

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { OtherDeviceKeysUpdateRequest } from "@bitwarden/common/auth/models/request/update-devices-trust.request";
@@ -15,51 +13,51 @@ export abstract class DeviceTrustServiceAbstraction {
* by Platform
* @description Checks if the device trust feature is supported for the active user.
*/
supportsDeviceTrust$: Observable<boolean>;
abstract supportsDeviceTrust$: Observable<boolean>;
/**
* Emits when a device has been trusted. This emission is specifically for the purpose of notifying
* the consuming component to display a toast informing the user the device has been trusted.
*/
deviceTrusted$: Observable<void>;
abstract deviceTrusted$: Observable<void>;
/**
* @description Checks if the device trust feature is supported for the given user.
*/
supportsDeviceTrustByUserId$: (userId: UserId) => Observable<boolean>;
abstract supportsDeviceTrustByUserId$(userId: UserId): Observable<boolean>;
/**
* @description Retrieves the users choice to trust the device which can only happen after decryption
* Note: this value should only be used once and then reset
*/
getShouldTrustDevice: (userId: UserId) => Promise<boolean | null>;
setShouldTrustDevice: (userId: UserId, value: boolean) => Promise<void>;
abstract getShouldTrustDevice(userId: UserId): Promise<boolean | null>;
abstract setShouldTrustDevice(userId: UserId, value: boolean): Promise<void>;
trustDeviceIfRequired: (userId: UserId) => Promise<void>;
abstract trustDeviceIfRequired(userId: UserId): Promise<void>;
trustDevice: (userId: UserId) => Promise<DeviceResponse>;
abstract trustDevice(userId: UserId): Promise<DeviceResponse>;
/** Retrieves the device key if it exists from state or secure storage if supported for the active user. */
getDeviceKey: (userId: UserId) => Promise<DeviceKey | null>;
decryptUserKeyWithDeviceKey: (
abstract getDeviceKey(userId: UserId): Promise<DeviceKey | null>;
abstract decryptUserKeyWithDeviceKey(
userId: UserId,
encryptedDevicePrivateKey: EncString,
encryptedUserKey: EncString,
deviceKey: DeviceKey,
) => Promise<UserKey | null>;
rotateDevicesTrust: (
): Promise<UserKey | null>;
abstract rotateDevicesTrust(
userId: UserId,
newUserKey: UserKey,
masterPasswordHash: string,
) => Promise<void>;
): Promise<void>;
/**
* Notifies the server that the device has a device key, but didn't receive any associated decryption keys.
* Note: For debugging purposes only.
*/
recordDeviceTrustLoss: () => Promise<void>;
getRotatedData: (
abstract recordDeviceTrustLoss(): Promise<void>;
abstract getRotatedData(
oldUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
) => Promise<OtherDeviceKeysUpdateRequest[]>;
): Promise<OtherDeviceKeysUpdateRequest[]>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "../../../types/guid";
@@ -13,11 +11,11 @@ export abstract class VaultTimeoutSettingsService {
* @param vaultTimeoutAction The vault timeout action
* @param userId The user id to set the data for.
*/
setVaultTimeoutOptions: (
abstract setVaultTimeoutOptions(
userId: UserId,
vaultTimeout: VaultTimeout,
vaultTimeoutAction: VaultTimeoutAction,
) => Promise<void>;
): Promise<void>;
/**
* Get the available vault timeout actions for the current user
@@ -25,13 +23,13 @@ export abstract class VaultTimeoutSettingsService {
* **NOTE:** This observable is not yet connected to the state service, so it will not update when the state changes
* @param userId The user id to check. If not provided, the current user is used
*/
availableVaultTimeoutActions$: (userId?: string) => Observable<VaultTimeoutAction[]>;
abstract availableVaultTimeoutActions$(userId?: string): Observable<VaultTimeoutAction[]>;
/**
* Evaluates the user's available vault timeout actions and returns a boolean representing
* if the user can lock or not
*/
canLock: (userId: string) => Promise<boolean>;
abstract canLock(userId: string): Promise<boolean>;
/**
* Gets the vault timeout action for the given user id. The returned value is
@@ -41,7 +39,7 @@ export abstract class VaultTimeoutSettingsService {
* A new action will be emitted if the current state changes or if the user's policy changes and the new policy affects the action.
* @param userId - the user id to get the vault timeout action for
*/
getVaultTimeoutActionByUserId$: (userId: string) => Observable<VaultTimeoutAction>;
abstract getVaultTimeoutActionByUserId$(userId: string): Observable<VaultTimeoutAction>;
/**
* Get the vault timeout for the given user id. The returned value is calculated based on the current state
@@ -50,14 +48,14 @@ export abstract class VaultTimeoutSettingsService {
* A new timeout will be emitted if the current state changes or if the user's policy changes and the new policy affects the timeout.
* @param userId The user id to get the vault timeout for
*/
getVaultTimeoutByUserId$: (userId: string) => Observable<VaultTimeout>;
abstract getVaultTimeoutByUserId$(userId: string): Observable<VaultTimeout>;
/**
* Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used
* @returns boolean true if biometric lock is set
*/
isBiometricLockSet: (userId?: string) => Promise<boolean>;
abstract isBiometricLockSet(userId?: string): Promise<boolean>;
clear: (userId: UserId) => Promise<void>;
abstract clear(userId: UserId): Promise<void>;
}

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export abstract class VaultTimeoutService {
checkVaultTimeout: () => Promise<void>;
lock: (userId?: string) => Promise<void>;
logOut: (userId?: string) => Promise<void>;
abstract checkVaultTimeout(): Promise<void>;
abstract lock(userId?: string): Promise<void>;
abstract logOut(userId?: string): Promise<void>;
}

View File

@@ -143,10 +143,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
),
);
if (userId == null || userId === currentUserId) {
await this.collectionService.clearActiveUserCache();
}
await this.searchService.clearIndex(lockingUserId);
await this.folderService.clearDecryptedFolderState(lockingUserId);

View File

@@ -56,6 +56,7 @@ export class CipherExport {
view.notes = req.notes;
view.favorite = req.favorite;
view.reprompt = req.reprompt ?? CipherRepromptType.None;
view.key = req.key != null ? new EncString(req.key) : null;
if (req.fields != null) {
view.fields = req.fields.map((f) => FieldExport.toView(f));
@@ -83,9 +84,9 @@ export class CipherExport {
view.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toView(ph));
}
view.creationDate = req.creationDate;
view.revisionDate = req.revisionDate;
view.deletedDate = req.deletedDate;
view.creationDate = req.creationDate ? new Date(req.creationDate) : null;
view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
return view;
}

View File

@@ -16,7 +16,7 @@ export class PasswordHistoryExport {
static toView(req: PasswordHistoryExport, view = new PasswordHistoryView()) {
view.password = req.password;
view.lastUsedDate = req.lastUsedDate;
view.lastUsedDate = req.lastUsedDate ? new Date(req.lastUsedDate) : null;
return view;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, Subject } from "rxjs";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
@@ -25,13 +23,13 @@ export interface ActiveRequest {
export type RequestCollection = Readonly<{ [tabId: number]: ActiveRequest }>;
export abstract class Fido2ActiveRequestManager {
getActiveRequest$: (tabId: number) => Observable<ActiveRequest | undefined>;
getActiveRequest: (tabId: number) => ActiveRequest | undefined;
newActiveRequest: (
abstract getActiveRequest$(tabId: number): Observable<ActiveRequest | undefined>;
abstract getActiveRequest(tabId: number): ActiveRequest | undefined;
abstract newActiveRequest(
tabId: number,
credentials: Fido2CredentialView[],
abortController: AbortController,
) => Promise<RequestResult>;
removeActiveRequest: (tabId: number) => void;
removeAllActiveRequests: () => void;
): Promise<RequestResult>;
abstract removeActiveRequest(tabId: number): void;
abstract removeAllActiveRequests(): void;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
/**
@@ -17,11 +15,11 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the new credential and an attestation signature.
**/
makeCredential: (
abstract makeCredential(
params: Fido2AuthenticatorMakeCredentialsParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorMakeCredentialResult>;
): Promise<Fido2AuthenticatorMakeCredentialResult>;
/**
* Generate an assertion using an existing credential as describe in:
@@ -31,11 +29,11 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the asserted credential and an assertion signature.
*/
getAssertion: (
abstract getAssertion(
params: Fido2AuthenticatorGetAssertionParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2AuthenticatorGetAssertionResult>;
): Promise<Fido2AuthenticatorGetAssertionResult>;
/**
* Discover credentials for a given Relying Party
@@ -43,7 +41,7 @@ export abstract class Fido2AuthenticatorService<ParentWindowReference> {
* @param rpId The Relying Party's ID
* @returns A promise that resolves with an array of discoverable credentials
*/
silentCredentialDiscovery: (rpId: string) => Promise<Fido2CredentialView[]>;
abstract silentCredentialDiscovery(rpId: string): Promise<Fido2CredentialView[]>;
}
// FIXME: update to use a const object instead of a typescript enum

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export const UserRequestedFallbackAbortReason = "UserRequestedFallback";
export type UserVerification = "discouraged" | "preferred" | "required";
@@ -16,7 +14,7 @@ export type UserVerification = "discouraged" | "preferred" | "required";
* and for returning the results of the latter operations to the Web Authentication API's callers.
*/
export abstract class Fido2ClientService<ParentWindowReference> {
isFido2FeatureEnabled: (hostname: string, origin: string) => Promise<boolean>;
abstract isFido2FeatureEnabled(hostname: string, origin: string): Promise<boolean>;
/**
* Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
@@ -26,11 +24,11 @@ export abstract class Fido2ClientService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the new credential.
*/
createCredential: (
abstract createCredential(
params: CreateCredentialParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<CreateCredentialResult>;
): Promise<CreateCredentialResult>;
/**
* Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent.
@@ -41,11 +39,11 @@ export abstract class Fido2ClientService<ParentWindowReference> {
* @param abortController An AbortController that can be used to abort the operation.
* @returns A promise that resolves with the asserted credential.
*/
assertCredential: (
abstract assertCredential(
params: AssertCredentialParams,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<AssertCredentialResult>;
): Promise<AssertCredentialResult>;
}
/**

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
/**
* Parameters used to ask the user to confirm the creation of a new credential.
*/
@@ -69,11 +67,11 @@ export abstract class Fido2UserInterfaceService<ParentWindowReference> {
* @param fallbackSupported Whether or not the browser natively supports WebAuthn.
* @param abortController An abort controller that can be used to cancel/close the session.
*/
newSession: (
abstract newSession(
fallbackSupported: boolean,
window: ParentWindowReference,
abortController?: AbortController,
) => Promise<Fido2UserInterfaceSession>;
): Promise<Fido2UserInterfaceSession>;
}
export abstract class Fido2UserInterfaceSession {
@@ -84,9 +82,9 @@ export abstract class Fido2UserInterfaceSession {
* @param abortController An abort controller that can be used to cancel/close the session.
* @returns The ID of the cipher that contains the credentials the user picked. If not cipher was picked, return cipherId = undefined to to let the authenticator throw the error.
*/
pickCredential: (
abstract pickCredential(
params: PickCredentialParams,
) => Promise<{ cipherId: string; userVerified: boolean }>;
): Promise<{ cipherId: string; userVerified: boolean }>;
/**
* Ask the user to confirm the creation of a new credential.
@@ -95,30 +93,30 @@ export abstract class Fido2UserInterfaceSession {
* @param abortController An abort controller that can be used to cancel/close the session.
* @returns The ID of the cipher where the new credential should be saved.
*/
confirmNewCredential: (
abstract confirmNewCredential(
params: NewCredentialParams,
) => Promise<{ cipherId: string; userVerified: boolean }>;
): Promise<{ cipherId: string; userVerified: boolean }>;
/**
* Make sure that the vault is unlocked.
* This will open a window and ask the user to login or unlock the vault if necessary.
*/
ensureUnlockedVault: () => Promise<void>;
abstract ensureUnlockedVault(): Promise<void>;
/**
* Inform the user that the operation was cancelled because their vault contains excluded credentials.
*
* @param existingCipherIds The IDs of the excluded credentials.
*/
informExcludedCredential: (existingCipherIds: string[]) => Promise<void>;
abstract informExcludedCredential(existingCipherIds: string[]): Promise<void>;
/**
* Inform the user that the operation was cancelled because their vault does not contain any useable credentials.
*/
informCredentialNotFound: (abortController?: AbortController) => Promise<void>;
abstract informCredentialNotFound(abortController?: AbortController): Promise<void>;
/**
* Close the session, including any windows that may be open.
*/
close: () => void;
abstract close(): void;
}

View File

@@ -1,3 +1,3 @@
// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR,
// team specific PR's will come after.
export { MessageSender as MessagingService } from "../messaging/message.sender";
export { MessageSender as MessagingService } from "@bitwarden/messaging";

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { BiometricKey } from "../../auth/types/biometric-key";
import { Account } from "../models/domain/account";
import { StorageOptions } from "../models/domain/storage-options";
@@ -19,47 +17,47 @@ export type InitOptions = {
};
export abstract class StateService<T extends Account = Account> {
addAccount: (account: T) => Promise<void>;
clean: (options?: StorageOptions) => Promise<void>;
init: (initOptions?: InitOptions) => Promise<void>;
abstract addAccount(account: T): Promise<void>;
abstract clean(options?: StorageOptions): Promise<void>;
abstract init(initOptions?: InitOptions): Promise<void>;
/**
* Gets the user's auto key
*/
getUserKeyAutoUnlock: (options?: StorageOptions) => Promise<string>;
abstract getUserKeyAutoUnlock(options?: StorageOptions): Promise<string>;
/**
* Sets the user's auto key
*/
setUserKeyAutoUnlock: (value: string | null, options?: StorageOptions) => Promise<void>;
abstract setUserKeyAutoUnlock(value: string | null, options?: StorageOptions): Promise<void>;
/**
* Gets the user's biometric key
*/
getUserKeyBiometric: (options?: StorageOptions) => Promise<string>;
abstract getUserKeyBiometric(options?: StorageOptions): Promise<string>;
/**
* Checks if the user has a biometric key available
*/
hasUserKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
abstract hasUserKeyBiometric(options?: StorageOptions): Promise<boolean>;
/**
* Sets the user's biometric key
*/
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
abstract setUserKeyBiometric(value: BiometricKey, options?: StorageOptions): Promise<void>;
/**
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
*/
setEnableDuckDuckGoBrowserIntegration: (
abstract setEnableDuckDuckGoBrowserIntegration(
value: boolean,
options?: StorageOptions,
) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
): Promise<void>;
abstract getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string>;
abstract setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void>;
/**
* @deprecated Use `TokenService.hasAccessToken$()` or `AuthService.authStatusFor$` instead.
*/
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
abstract getIsAuthenticated(options?: StorageOptions): Promise<boolean>;
/**
* @deprecated Use `AccountService.activeAccount$` instead.
*/
getUserId: (options?: StorageOptions) => Promise<string>;
abstract getUserId(options?: StorageOptions): Promise<string>;
}

View File

@@ -1,46 +0,0 @@
import { Subject, firstValueFrom } from "rxjs";
import { getCommand, isExternalMessage, tagAsExternal } from "./helpers";
import { Message, CommandDefinition } from "./types";
describe("helpers", () => {
describe("getCommand", () => {
it("can get the command from just a string", () => {
const command = getCommand("myCommand");
expect(command).toEqual("myCommand");
});
it("can get the command from a message definition", () => {
const commandDefinition = new CommandDefinition<Record<string, unknown>>("myCommand");
const command = getCommand(commandDefinition);
expect(command).toEqual("myCommand");
});
});
describe("tag integration", () => {
it("can tag and identify as tagged", async () => {
const messagesSubject = new Subject<Message<Record<string, unknown>>>();
const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal());
const firstValuePromise = firstValueFrom(taggedMessages);
messagesSubject.next({ command: "test" });
const result = await firstValuePromise;
expect(isExternalMessage(result)).toEqual(true);
});
});
describe("isExternalMessage", () => {
it.each([null, { command: "myCommand", test: "object" }, undefined] as Message<
Record<string, unknown>
>[])("returns false when value is %s", (value: Message<Record<string, unknown>>) => {
expect(isExternalMessage(value)).toBe(false);
});
});
});

View File

@@ -1,25 +0,0 @@
import { map } from "rxjs";
import { CommandDefinition } from "./types";
export const getCommand = (
commandDefinition: CommandDefinition<Record<string, unknown>> | string,
) => {
if (typeof commandDefinition === "string") {
return commandDefinition;
} else {
return commandDefinition.command;
}
};
export const EXTERNAL_SOURCE_TAG = Symbol("externalSource");
export const isExternalMessage = (message: Record<PropertyKey, unknown>) => {
return message?.[EXTERNAL_SOURCE_TAG] === true;
};
export const tagAsExternal = <T extends Record<PropertyKey, unknown>>() => {
return map((message: T) => {
return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true });
});
};

View File

@@ -1,4 +1 @@
export { MessageListener } from "./message.listener";
export { MessageSender } from "./message.sender";
export { Message, CommandDefinition } from "./types";
export { isExternalMessage } from "./helpers";
export * from "@bitwarden/messaging";

View File

@@ -1,5 +1 @@
// Built in implementations
export { SubjectMessageSender } from "./subject-message.sender";
// Helpers meant to be used only by other implementations
export { tagAsExternal, getCommand } from "./helpers";
export * from "@bitwarden/messaging-internal";

View File

@@ -1,47 +0,0 @@
import { Subject } from "rxjs";
import { subscribeTo } from "../../../spec/observable-tracker";
import { MessageListener } from "./message.listener";
import { Message, CommandDefinition } from "./types";
describe("MessageListener", () => {
const subject = new Subject<Message<{ test: number }>>();
const sut = new MessageListener(subject.asObservable());
const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand");
describe("allMessages$", () => {
it("runs on all nexts", async () => {
const tracker = subscribeTo(sut.allMessages$);
const pausePromise = tracker.pauseUntilReceived(2);
subject.next({ command: "command1", test: 1 });
subject.next({ command: "command2", test: 2 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 });
expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 });
});
});
describe("messages$", () => {
it("runs on only my commands", async () => {
const tracker = subscribeTo(sut.messages$(testCommandDefinition));
const pausePromise = tracker.pauseUntilReceived(2);
subject.next({ command: "notMyCommand", test: 1 });
subject.next({ command: "myCommand", test: 2 });
subject.next({ command: "myCommand", test: 3 });
subject.next({ command: "notMyCommand", test: 4 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 });
expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 });
});
});
});

View File

@@ -1,43 +0,0 @@
import { EMPTY, Observable, filter } from "rxjs";
import { Message, CommandDefinition } from "./types";
/**
* A class that allows for listening to messages coming through the application,
* allows for listening of all messages or just the messages you care about.
*
* @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of
* data that is persisted. This can serve messages that might have been used to notify of settings changes
* or vault data changes and those observables should be preferred over messaging.
*/
export class MessageListener {
constructor(private readonly messageStream: Observable<Message<Record<string, unknown>>>) {}
/**
* A stream of all messages sent through the application. It does not contain type information for the
* other properties on the messages. You are encouraged to instead subscribe to an individual message
* through {@link messages$}.
*/
allMessages$ = this.messageStream;
/**
* Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed
* to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being
* sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message
* payload being the expected type.
*
* @param commandDefinition The CommandDefinition containing the information about the message type you care about.
*/
messages$<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T>,
): Observable<T> {
return this.allMessages$.pipe(
filter((msg) => msg?.command === commandDefinition.command),
) as Observable<T>;
}
/**
* A helper property for returning a MessageListener that will never emit any messages and will immediately complete.
*/
static readonly EMPTY = new MessageListener(EMPTY);
}

View File

@@ -1,65 +0,0 @@
import { CommandDefinition } from "./types";
class MultiMessageSender implements MessageSender {
constructor(private readonly innerMessageSenders: MessageSender[]) {}
send<T extends Record<string, unknown>>(
commandDefinition: string | CommandDefinition<T>,
payload: Record<string, unknown> | T = {},
): void {
for (const messageSender of this.innerMessageSenders) {
messageSender.send(commandDefinition, payload);
}
}
}
export abstract class MessageSender {
/**
* A method for sending messages in a type safe manner. The passed in command definition
* will require you to provide a compatible type in the payload parameter.
*
* @example
* const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand");
*
* this.messageSender.send(MY_COMMAND, { test: 14 });
*
* @param commandDefinition
* @param payload
*/
abstract send<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T>,
payload: T,
): void;
/**
* A legacy method for sending messages in a non-type safe way.
*
* @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to
* get compilation errors when defining an incompatible payload.
*
* @param command The string based command of your message.
* @param payload Extra contextual information regarding the message. Be aware that this payload may
* be serialized and lose all prototype information.
*/
abstract send(command: string, payload?: Record<string, unknown>): void;
/** Implementation of the other two overloads, read their docs instead. */
abstract send<T extends Record<string, unknown>>(
commandDefinition: CommandDefinition<T> | string,
payload: T | Record<string, unknown>,
): void;
/**
* A helper method for combine multiple {@link MessageSender}'s.
* @param messageSenders The message senders that should be combined.
* @returns A message sender that will relay all messages to the given message senders.
*/
static combine(...messageSenders: MessageSender[]) {
return new MultiMessageSender(messageSenders);
}
/**
* A helper property for creating a {@link MessageSender} that sends to nowhere.
*/
static readonly EMPTY: MessageSender = new MultiMessageSender([]);
}

View File

@@ -1,65 +0,0 @@
import { Subject } from "rxjs";
import { subscribeTo } from "../../../spec/observable-tracker";
import { SubjectMessageSender } from "./internal";
import { MessageSender } from "./message.sender";
import { Message, CommandDefinition } from "./types";
describe("SubjectMessageSender", () => {
const subject = new Subject<Message<{ test: number }>>();
const subjectObservable = subject.asObservable();
const sut: MessageSender = new SubjectMessageSender(subject);
describe("send", () => {
it("will send message with command from message definition", async () => {
const commandDefinition = new CommandDefinition<{ test: number }>("myCommand");
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send(commandDefinition, { test: 1 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
});
it("will send message with command from normal string", async () => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand", { test: 1 });
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 });
});
it("will send message with object even if payload not given", async () => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand");
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
});
it.each([null, undefined])(
"will send message with object even if payload is null-ish (%s)",
async (payloadValue) => {
const tracker = subscribeTo(subjectObservable);
const pausePromise = tracker.pauseUntilReceived(1);
sut.send("myCommand", payloadValue);
await pausePromise;
expect(tracker.emissions[0]).toEqual({ command: "myCommand" });
},
);
});
});

View File

@@ -1,17 +0,0 @@
import { Subject } from "rxjs";
import { getCommand } from "./internal";
import { MessageSender } from "./message.sender";
import { Message, CommandDefinition } from "./types";
export class SubjectMessageSender implements MessageSender {
constructor(private readonly messagesSubject: Subject<Message<Record<string, unknown>>>) {}
send<T extends Record<string, unknown>>(
commandDefinition: string | CommandDefinition<T>,
payload: Record<string, unknown> | T = {},
): void {
const command = getCommand(commandDefinition);
this.messagesSubject.next(Object.assign(payload ?? {}, { command: command }));
}
}

View File

@@ -1,15 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
declare const tag: unique symbol;
/**
* A class for defining information about a message, this is helpful
* alonside `MessageSender` and `MessageListener` for providing a type
* safe(-ish) way of sending and receiving messages.
*/
export class CommandDefinition<T extends Record<string, unknown>> {
[tag]: T;
constructor(readonly command: string) {}
}
export type Message<T extends Record<string, unknown>> = { command: string } & T;

View File

@@ -13,9 +13,9 @@ export const getById = <TId, T extends { id: TId }>(id: TId) =>
* @param id The IDs of the objects to return.
* @returns An array containing objects with matching IDs, or an empty array if there are no matching objects.
*/
export const getByIds = <TId, T extends { id: TId }>(ids: TId[]) => {
const idSet = new Set(ids);
export const getByIds = <TId, T extends { id: TId | undefined }>(ids: TId[]) => {
const idSet = new Set(ids.filter((id) => id != null));
return map<T[], T[]>((objects) => {
return objects.filter((o) => idSet.has(o.id));
return objects.filter((o) => o.id && idSet.has(o.id));
});
};

View File

@@ -14,7 +14,7 @@ export type DecryptedObject<
> = Record<TDecryptedKeys, string> & Omit<TEncryptedObject, TDecryptedKeys>;
// extracts shared keys from the domain and view types
type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
export type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
ConditionalKeys<D, EncString | null>) &
(keyof V & ConditionalKeys<V, string | null>);

View File

@@ -228,6 +228,7 @@ export class DefaultSdkService implements SdkService {
},
privateKey,
signingKey: undefined,
securityState: undefined,
});
// We initialize the org crypto even if the org_keys are

View File

@@ -164,9 +164,13 @@ export const SEND_ACCESS_AUTH_MEMORY = new StateDefinition("sendAccessAuth", "me
// Vault
export const COLLECTION_DATA = new StateDefinition("collection", "disk", {
export const COLLECTION_DISK = new StateDefinition("collection", "disk", {
web: "memory",
});
export const COLLECTION_MEMORY = new StateDefinition("decryptedCollections", "memory", {
browser: "memory-large-object",
});
export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" });
export const FOLDER_MEMORY = new StateDefinition("decryptedFolders", "memory", {
browser: "memory-large-object",
@@ -202,6 +206,13 @@ export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk");
export const AT_RISK_PASSWORDS_PAGE_DISK = new StateDefinition("atRiskPasswordsPage", "disk");
export const NOTIFICATION_DISK = new StateDefinition("notifications", "disk");
export const NUDGES_DISK = new StateDefinition("nudges", "disk", { web: "disk-local" });
export const SETUP_EXTENSION_DISMISSED_DISK = new StateDefinition(
"setupExtensionDismissed",
"disk",
{
web: "disk-local",
},
);
export const VAULT_BROWSER_INTRO_CAROUSEL = new StateDefinition(
"vaultBrowserIntroCarousel",
"disk",

View File

@@ -172,7 +172,11 @@ export abstract class CoreSyncService implements SyncService {
notification.collectionIds != null &&
notification.collectionIds.length > 0
) {
const collections = await this.collectionService.getAll();
const collections = await firstValueFrom(
this.collectionService
.encryptedCollections$(userId)
.pipe(map((collections) => collections ?? [])),
);
if (collections != null) {
for (let i = 0; i < collections.length; i++) {
if (notification.collectionIds.indexOf(collections[i].id) > -1) {

View File

@@ -1,7 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ZXCVBNResult } from "zxcvbn";
export abstract class PasswordStrengthServiceAbstraction {
getPasswordStrength: (password: string, email?: string, userInputs?: string[]) => ZXCVBNResult;
abstract getPasswordStrength(
password: string,
email?: string,
userInputs?: string[],
): ZXCVBNResult;
}

View File

@@ -21,6 +21,7 @@ export class SendData {
expirationDate: string;
deletionDate: string;
password: string;
emails: string;
disabled: boolean;
hideEmail: boolean;
@@ -41,6 +42,7 @@ export class SendData {
this.expirationDate = response.expirationDate;
this.deletionDate = response.deletionDate;
this.password = response.password;
this.emails = response.emails;
this.disabled = response.disable;
this.hideEmail = response.hideEmail;

View File

@@ -29,14 +29,15 @@ describe("Send", () => {
text: "encText",
hidden: true,
},
file: null,
file: null!,
key: "encKey",
maxAccessCount: null,
maxAccessCount: null!,
accessCount: 10,
revisionDate: "2022-01-31T12:00:00.000Z",
expirationDate: "2022-01-31T12:00:00.000Z",
deletionDate: "2022-01-31T12:00:00.000Z",
password: "password",
emails: null!,
disabled: false,
hideEmail: true,
};
@@ -86,6 +87,7 @@ describe("Send", () => {
expirationDate: new Date("2022-01-31T12:00:00.000Z"),
deletionDate: new Date("2022-01-31T12:00:00.000Z"),
password: "password",
emails: null!,
disabled: false,
hideEmail: true,
});

View File

@@ -27,6 +27,7 @@ export class Send extends Domain {
expirationDate: Date;
deletionDate: Date;
password: string;
emails: string;
disabled: boolean;
hideEmail: boolean;
@@ -53,6 +54,7 @@ export class Send extends Domain {
this.maxAccessCount = obj.maxAccessCount;
this.accessCount = obj.accessCount;
this.password = obj.password;
this.emails = obj.emails;
this.disabled = obj.disabled;
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null;

View File

@@ -17,6 +17,7 @@ export class SendRequest {
text: SendTextApi;
file: SendFileApi;
password: string;
emails: string;
disabled: boolean;
hideEmail: boolean;
@@ -30,6 +31,7 @@ export class SendRequest {
this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null;
this.key = send.key != null ? send.key.encryptedString : null;
this.password = send.password;
this.emails = send.emails;
this.disabled = send.disabled;
this.hideEmail = send.hideEmail;

View File

@@ -20,6 +20,7 @@ export class SendResponse extends BaseResponse {
expirationDate: string;
deletionDate: string;
password: string;
emails: string;
disable: boolean;
hideEmail: boolean;
@@ -37,6 +38,7 @@ export class SendResponse extends BaseResponse {
this.expirationDate = this.getResponseProperty("ExpirationDate");
this.deletionDate = this.getResponseProperty("DeletionDate");
this.password = this.getResponseProperty("Password");
this.emails = this.getResponseProperty("Emails");
this.disable = this.getResponseProperty("Disabled") || false;
this.hideEmail = this.getResponseProperty("HideEmail") || false;

View File

@@ -26,6 +26,7 @@ export class SendView implements View {
deletionDate: Date = null;
expirationDate: Date = null;
password: string = null;
emails: string[] = [];
disabled = false;
hideEmail = false;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ListResponse } from "../../../models/response/list.response";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
import { Send } from "../models/domain/send";
@@ -12,26 +10,29 @@ import { SendResponse } from "../models/response/send.response";
import { SendAccessView } from "../models/view/send-access.view";
export abstract class SendApiService {
getSend: (id: string) => Promise<SendResponse>;
postSendAccess: (
abstract getSend(id: string): Promise<SendResponse>;
abstract postSendAccess(
id: string,
request: SendAccessRequest,
apiUrl?: string,
) => Promise<SendAccessResponse>;
getSends: () => Promise<ListResponse<SendResponse>>;
postSend: (request: SendRequest) => Promise<SendResponse>;
postFileTypeSend: (request: SendRequest) => Promise<SendFileUploadDataResponse>;
postSendFile: (sendId: string, fileId: string, data: FormData) => Promise<any>;
putSend: (id: string, request: SendRequest) => Promise<SendResponse>;
putSendRemovePassword: (id: string) => Promise<SendResponse>;
deleteSend: (id: string) => Promise<any>;
getSendFileDownloadData: (
): Promise<SendAccessResponse>;
abstract getSends(): Promise<ListResponse<SendResponse>>;
abstract postSend(request: SendRequest): Promise<SendResponse>;
abstract postFileTypeSend(request: SendRequest): Promise<SendFileUploadDataResponse>;
abstract postSendFile(sendId: string, fileId: string, data: FormData): Promise<any>;
abstract putSend(id: string, request: SendRequest): Promise<SendResponse>;
abstract putSendRemovePassword(id: string): Promise<SendResponse>;
abstract deleteSend(id: string): Promise<any>;
abstract getSendFileDownloadData(
send: SendAccessView,
request: SendAccessRequest,
apiUrl?: string,
) => Promise<SendFileDownloadDataResponse>;
renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise<SendFileUploadDataResponse>;
removePassword: (id: string) => Promise<any>;
delete: (id: string) => Promise<any>;
save: (sendData: [Send, EncArrayBuffer]) => Promise<Send>;
): Promise<SendFileDownloadDataResponse>;
abstract renewSendFileUploadUrl(
sendId: string,
fileId: string,
): Promise<SendFileUploadDataResponse>;
abstract removePassword(id: string): Promise<any>;
abstract delete(id: string): Promise<any>;
abstract save(sendData: [Send, EncArrayBuffer]): Promise<Send>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
@@ -16,49 +14,49 @@ import { SendWithIdRequest } from "../models/request/send-with-id.request";
import { SendView } from "../models/view/send.view";
export abstract class SendService implements UserKeyRotationDataProvider<SendWithIdRequest> {
sends$: Observable<Send[]>;
sendViews$: Observable<SendView[]>;
abstract sends$: Observable<Send[]>;
abstract sendViews$: Observable<SendView[]>;
encrypt: (
abstract encrypt(
model: SendView,
file: File | ArrayBuffer,
password: string,
key?: SymmetricCryptoKey,
) => Promise<[Send, EncArrayBuffer]>;
): Promise<[Send, EncArrayBuffer]>;
/**
* Provides a send for a determined id
* updates after a change occurs to the send that matches the id
* @param id The id of the desired send
* @returns An observable that listens to the value of the desired send
*/
get$: (id: string) => Observable<Send | undefined>;
abstract get$(id: string): Observable<Send | undefined>;
/**
* Provides re-encrypted user sends for the key rotation process
* @param newUserKey The new user key to use for re-encryption
* @throws Error if the new user key is null or undefined
* @returns A list of user sends that have been re-encrypted with the new user key
*/
getRotatedData: (
abstract getRotatedData(
originalUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
) => Promise<SendWithIdRequest[]>;
): Promise<SendWithIdRequest[]>;
/**
* @deprecated Do not call this, use the sends$ observable collection
*/
getAll: () => Promise<Send[]>;
abstract getAll(): Promise<Send[]>;
/**
* @deprecated Only use in CLI
*/
getFromState: (id: string) => Promise<Send>;
abstract getFromState(id: string): Promise<Send>;
/**
* @deprecated Only use in CLI
*/
getAllDecryptedFromState: (userId: UserId) => Promise<SendView[]>;
abstract getAllDecryptedFromState(userId: UserId): Promise<SendView[]>;
}
export abstract class InternalSendService extends SendService {
upsert: (send: SendData | SendData[]) => Promise<any>;
replace: (sends: { [id: string]: SendData }, userId: UserId) => Promise<void>;
delete: (id: string | string[]) => Promise<any>;
abstract upsert(send: SendData | SendData[]): Promise<any>;
abstract replace(sends: { [id: string]: SendData }, userId: UserId): Promise<void>;
abstract delete(id: string | string[]): Promise<any>;
}

View File

@@ -74,7 +74,12 @@ export class SendService implements InternalSendServiceAbstraction {
model.key = key.material;
model.cryptoKey = key.derivedKey;
}
if (password != null) {
const hasEmails = (model.emails?.length ?? 0) > 0;
if (hasEmails) {
send.emails = model.emails.join(",");
send.password = null;
} else if (password != null) {
// Note: Despite being called key, the passwordKey is not used for encryption.
// It is used as a static proof that the client knows the password, and has the encryption key.
const passwordKey = await this.keyGenerationService.deriveKeyFromPassword(
@@ -89,10 +94,13 @@ export class SendService implements InternalSendServiceAbstraction {
}
// Key is not a SymmetricCryptoKey, but key material used to derive the cryptoKey
send.key = await this.encryptService.encryptBytes(model.key, userKey);
// FIXME: model.name can be null. encryptString should not be called with null values.
send.name = await this.encryptService.encryptString(model.name, model.cryptoKey);
// FIXME: model.notes can be null. encryptString should not be called with null values.
send.notes = await this.encryptService.encryptString(model.notes, model.cryptoKey);
if (send.type === SendType.Text) {
send.text = new SendText();
// FIXME: model.text.text can be null. encryptString should not be called with null values.
send.text.text = await this.encryptService.encryptString(model.text.text, model.cryptoKey);
send.text.hidden = model.text.hidden;
} else if (send.type === SendType.File) {

View File

@@ -1,6 +1,7 @@
import { EncryptionContext } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherListView } from "@bitwarden/sdk-internal";
import { UserId } from "../../types/guid";
import { UserId, OrganizationId } from "../../types/guid";
import { Cipher } from "../models/domain/cipher";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
@@ -9,6 +10,28 @@ import { CipherView } from "../models/view/cipher.view";
* Service responsible for encrypting and decrypting ciphers.
*/
export abstract class CipherEncryptionService {
/**
* Encrypts a cipher using the SDK for the given userId.
* @param model The cipher view to encrypt
* @param userId The user ID to initialize the SDK client with
*
* @returns A promise that resolves to the encryption context, or undefined if encryption fails
*/
abstract encrypt(model: CipherView, userId: UserId): Promise<EncryptionContext | undefined>;
/**
* Move the cipher to the specified organization by re-encrypting its keys with the organization's key.
* The cipher.organizationId will be updated to the new organizationId.
* @param model The cipher view to move to the organization
* @param organizationId The ID of the organization to move the cipher to
* @param userId The user ID to initialize the SDK client with
*/
abstract moveToOrganization(
model: CipherView,
organizationId: OrganizationId,
userId: UserId,
): Promise<EncryptionContext | undefined>;
/**
* Decrypts a cipher using the SDK for the given userId.
*

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
@@ -120,11 +118,21 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
orgAdmin?: boolean,
isNotClone?: boolean,
): Promise<Cipher>;
/**
* Move a cipher to an organization by re-encrypting its keys with the organization's key.
* @param cipher The cipher to move
* @param organizationId The Id of the organization to move the cipher to
* @param collectionIds The collection Ids to assign the cipher to in the organization
* @param userId The Id of the user performing the operation
* @param originalCipher Optional original cipher that will be used to compare/update password history
*/
abstract shareWithServer(
cipher: CipherView,
organizationId: string,
collectionIds: string[],
userId: UserId,
originalCipher?: Cipher,
): Promise<Cipher>;
abstract shareManyWithServer(
ciphers: CipherView[],

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -7,11 +5,11 @@ import { Cipher } from "../../models/domain/cipher";
import { CipherResponse } from "../../models/response/cipher.response";
export abstract class CipherFileUploadService {
upload: (
abstract upload(
cipher: Cipher,
encFileName: EncString,
encData: EncArrayBuffer,
admin: boolean,
dataEncKey: [SymmetricCryptoKey, EncString],
) => Promise<CipherResponse>;
): Promise<CipherResponse>;
}

View File

@@ -1,14 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "../../../types/guid";
import { FolderData } from "../../models/data/folder.data";
import { Folder } from "../../models/domain/folder";
import { FolderResponse } from "../../models/response/folder.response";
export class FolderApiServiceAbstraction {
save: (folder: Folder, userId: UserId) => Promise<FolderData>;
delete: (id: string, userId: UserId) => Promise<any>;
get: (id: string) => Promise<FolderResponse>;
deleteAll: (userId: UserId) => Promise<void>;
export abstract class FolderApiServiceAbstraction {
abstract save(folder: Folder, userId: UserId): Promise<FolderData>;
abstract delete(id: string, userId: UserId): Promise<any>;
abstract get(id: string): Promise<FolderResponse>;
abstract deleteAll(userId: UserId): Promise<void>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
@@ -15,27 +13,27 @@ import { FolderWithIdRequest } from "../../models/request/folder-with-id.request
import { FolderView } from "../../models/view/folder.view";
export abstract class FolderService implements UserKeyRotationDataProvider<FolderWithIdRequest> {
folders$: (userId: UserId) => Observable<Folder[]>;
folderViews$: (userId: UserId) => Observable<FolderView[]>;
abstract folders$(userId: UserId): Observable<Folder[]>;
abstract folderViews$(userId: UserId): Observable<FolderView[]>;
clearDecryptedFolderState: (userId: UserId) => Promise<void>;
encrypt: (model: FolderView, key: SymmetricCryptoKey) => Promise<Folder>;
get: (id: string, userId: UserId) => Promise<Folder>;
getDecrypted$: (id: string, userId: UserId) => Observable<FolderView | undefined>;
abstract clearDecryptedFolderState(userId: UserId): Promise<void>;
abstract encrypt(model: FolderView, key: SymmetricCryptoKey): Promise<Folder>;
abstract get(id: string, userId: UserId): Promise<Folder>;
abstract getDecrypted$(id: string, userId: UserId): Observable<FolderView | undefined>;
/**
* @deprecated Use firstValueFrom(folders$) directly instead
* @param userId The user id
* @returns Promise of folders array
*/
getAllFromState: (userId: UserId) => Promise<Folder[]>;
abstract getAllFromState(userId: UserId): Promise<Folder[]>;
/**
* @deprecated Only use in CLI!
*/
getFromState: (id: string, userId: UserId) => Promise<Folder>;
abstract getFromState(id: string, userId: UserId): Promise<Folder>;
/**
* @deprecated Only use in CLI!
*/
getAllDecryptedFromState: (userId: UserId) => Promise<FolderView[]>;
abstract getAllDecryptedFromState(userId: UserId): Promise<FolderView[]>;
/**
* Returns user folders re-encrypted with the new user key.
* @param originalUserKey the original user key
@@ -44,16 +42,16 @@ export abstract class FolderService implements UserKeyRotationDataProvider<Folde
* @throws Error if new user key is null
* @returns a list of user folders that have been re-encrypted with the new user key
*/
getRotatedData: (
abstract getRotatedData(
originalUserKey: UserKey,
newUserKey: UserKey,
userId: UserId,
) => Promise<FolderWithIdRequest[]>;
): Promise<FolderWithIdRequest[]>;
}
export abstract class InternalFolderService extends FolderService {
upsert: (folder: FolderData | FolderData[], userId: UserId) => Promise<void>;
replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise<void>;
clear: (userId: UserId) => Promise<void>;
delete: (id: string | string[], userId: UserId) => Promise<any>;
abstract upsert(folder: FolderData | FolderData[], userId: UserId): Promise<void>;
abstract replace(folders: { [id: string]: FolderData }, userId: UserId): Promise<void>;
abstract clear(userId: UserId): Promise<void>;
abstract delete(id: string | string[], userId: UserId): Promise<any>;
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { SendView } from "../../tools/send/models/view/send.view";
@@ -8,25 +6,25 @@ import { CipherView } from "../models/view/cipher.view";
import { CipherViewLike } from "../utils/cipher-view-like-utils";
export abstract class SearchService {
indexedEntityId$: (userId: UserId) => Observable<IndexedEntityId | null>;
abstract indexedEntityId$(userId: UserId): Observable<IndexedEntityId | null>;
clearIndex: (userId: UserId) => Promise<void>;
isSearchable: (userId: UserId, query: string) => Promise<boolean>;
indexCiphers: (
abstract clearIndex(userId: UserId): Promise<void>;
abstract isSearchable(userId: UserId, query: string): Promise<boolean>;
abstract indexCiphers(
userId: UserId,
ciphersToIndex: CipherView[],
indexedEntityGuid?: string,
) => Promise<void>;
searchCiphers: <C extends CipherViewLike>(
): Promise<void>;
abstract searchCiphers<C extends CipherViewLike>(
userId: UserId,
query: string,
filter?: ((cipher: C) => boolean) | ((cipher: C) => boolean)[],
ciphers?: C[],
) => Promise<C[]>;
searchCiphersBasic: <C extends CipherViewLike>(
): Promise<C[]>;
abstract searchCiphersBasic<C extends CipherViewLike>(
ciphers: C[],
query: string,
deleted?: boolean,
) => C[];
searchSends: (sends: SendView[], query: string) => SendView[];
): C[];
abstract searchSends(sends: SendView[], query: string): SendView[];
}

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
/**
* Service for managing vault settings.
@@ -9,42 +7,40 @@ export abstract class VaultSettingsService {
* An observable monitoring the state of the enable passkeys setting.
* The observable updates when the setting changes.
*/
enablePasskeys$: Observable<boolean>;
abstract enablePasskeys$: Observable<boolean>;
/**
* An observable monitoring the state of the show cards on the current tab.
*/
showCardsCurrentTab$: Observable<boolean>;
abstract showCardsCurrentTab$: Observable<boolean>;
/**
* An observable monitoring the state of the show identities on the current tab.
*/
showIdentitiesCurrentTab$: Observable<boolean>;
/**
abstract showIdentitiesCurrentTab$: Observable<boolean>;
/**
* An observable monitoring the state of the click items on the Vault view
* for Autofill suggestions.
*/
clickItemsToAutofillVaultView$: Observable<boolean>;
/**
abstract clickItemsToAutofillVaultView$: Observable<boolean>;
/**
* Saves the enable passkeys setting to disk.
* @param value The new value for the passkeys setting.
*/
setEnablePasskeys: (value: boolean) => Promise<void>;
abstract setEnablePasskeys(value: boolean): Promise<void>;
/**
* Saves the show cards on tab page setting to disk.
* @param value The new value for the show cards on tab page setting.
*/
setShowCardsCurrentTab: (value: boolean) => Promise<void>;
abstract setShowCardsCurrentTab(value: boolean): Promise<void>;
/**
* Saves the show identities on tab page setting to disk.
* @param value The new value for the show identities on tab page setting.
*/
setShowIdentitiesCurrentTab: (value: boolean) => Promise<void>;
abstract setShowIdentitiesCurrentTab(value: boolean): Promise<void>;
/**
* Saves the click items on vault View for Autofill suggestions to disk.
* @param value The new value for the click items on vault View for
* Autofill suggestions setting.
*/
setClickItemsToAutofillVaultView: (value: boolean) => Promise<void>;
abstract setClickItemsToAutofillVaultView(value: boolean): Promise<void>;
}

View File

@@ -4,7 +4,7 @@ import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-intern
import { BaseResponse } from "../../../models/response/base.response";
export class CipherPermissionsApi extends BaseResponse {
export class CipherPermissionsApi extends BaseResponse implements SdkCipherPermissions {
delete: boolean = false;
restore: boolean = false;
@@ -35,4 +35,11 @@ export class CipherPermissionsApi extends BaseResponse {
return permissions;
}
/**
* Converts the CipherPermissionsApi to an SdkCipherPermissions
*/
toSdkCipherPermissions(): SdkCipherPermissions {
return this;
}
}

View File

@@ -1,4 +1,45 @@
import {
LocalDataView as SdkLocalDataView,
LocalData as SdkLocalData,
} from "@bitwarden/sdk-internal";
export type LocalData = {
lastUsedDate?: number;
lastLaunched?: number;
};
/**
* Convert the SdkLocalDataView to LocalData
* @param localData
*/
export function fromSdkLocalData(
localData: SdkLocalDataView | SdkLocalData | undefined,
): LocalData | undefined {
if (localData == null) {
return undefined;
}
return {
lastUsedDate: localData.lastUsedDate ? new Date(localData.lastUsedDate).getTime() : undefined,
lastLaunched: localData.lastLaunched ? new Date(localData.lastLaunched).getTime() : undefined,
};
}
/**
* Convert the LocalData to SdkLocalData
* @param localData
*/
export function toSdkLocalData(
localData: LocalData | undefined,
): (SdkLocalDataView & SdkLocalData) | undefined {
if (localData == null) {
return undefined;
}
return {
lastUsedDate: localData.lastUsedDate
? new Date(localData.lastUsedDate).toISOString()
: undefined,
lastLaunched: localData.lastLaunched
? new Date(localData.lastLaunched).toISOString()
: undefined,
};
}

View File

@@ -93,6 +93,7 @@ describe("Attachment", () => {
sizeName: "1.1 KB",
fileName: "fileName",
key: expect.any(SymmetricCryptoKey),
encryptedKey: attachment.key,
});
});

View File

@@ -56,6 +56,7 @@ export class Attachment extends Domain {
if (this.key != null) {
view.key = await this.decryptAttachmentKey(orgId, encKey);
view.encryptedKey = this.key; // Keep the encrypted key for the view
}
return view;
@@ -131,4 +132,24 @@ export class Attachment extends Domain {
key: this.key?.toJSON(),
};
}
/**
* Maps an SDK Attachment object to an Attachment
* @param obj - The SDK attachment object
*/
static fromSdkAttachment(obj: SdkAttachment): Attachment | undefined {
if (!obj) {
return undefined;
}
const attachment = new Attachment();
attachment.id = obj.id;
attachment.url = obj.url;
attachment.size = obj.size;
attachment.sizeName = obj.sizeName;
attachment.fileName = EncString.fromJSON(obj.fileName);
attachment.key = EncString.fromJSON(obj.key);
return attachment;
}
}

View File

@@ -103,4 +103,24 @@ export class Card extends Domain {
code: this.code?.toJSON(),
};
}
/**
* Maps an SDK Card object to a Card
* @param obj - The SDK Card object
*/
static fromSdkCard(obj: SdkCard): Card | undefined {
if (obj == null) {
return undefined;
}
const card = new Card();
card.cardholderName = EncString.fromJSON(obj.cardholderName);
card.brand = EncString.fromJSON(obj.brand);
card.number = EncString.fromJSON(obj.number);
card.expMonth = EncString.fromJSON(obj.expMonth);
card.expYear = EncString.fromJSON(obj.expYear);
card.code = EncString.fromJSON(obj.code);
return card;
}
}

View File

@@ -10,6 +10,7 @@ import {
UriMatchType,
CipherRepromptType as SdkCipherRepromptType,
LoginLinkedIdType,
Cipher as SdkCipher,
} from "@bitwarden/sdk-internal";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
@@ -206,7 +207,7 @@ describe("Cipher DTO", () => {
it("Convert", () => {
const cipher = new Cipher(cipherData);
expect(cipher).toEqual({
expect(cipher).toMatchObject({
initializerKey: InitializerKey.Cipher,
id: "id",
organizationId: "orgId" as OrganizationId,
@@ -339,9 +340,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
login: loginView,
attachments: null,
fields: null,
passwordHistory: null,
attachments: [],
fields: [],
passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -462,9 +463,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
secureNote: { type: 0 },
attachments: null,
fields: null,
passwordHistory: null,
attachments: [],
fields: [],
passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -603,9 +604,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
card: cardView,
attachments: null,
fields: null,
passwordHistory: null,
attachments: [],
fields: [],
passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -768,9 +769,9 @@ describe("Cipher DTO", () => {
edit: true,
viewPassword: true,
identity: identityView,
attachments: null,
fields: null,
passwordHistory: null,
attachments: [],
fields: [],
passwordHistory: [],
collectionIds: undefined,
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
creationDate: new Date("2022-01-01T12:00:00.000Z"),
@@ -1001,6 +1002,167 @@ describe("Cipher DTO", () => {
revisionDate: "2022-01-31T12:00:00.000Z",
});
});
it("should map from SDK Cipher", () => {
jest.restoreAllMocks();
const sdkCipher: SdkCipher = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
collectionIds: [],
key: "EncryptedString",
name: "EncryptedString",
notes: "EncryptedString",
type: SdkCipherType.Login,
login: {
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
uris: [
{
uri: "EncryptedString",
uriChecksum: "EncryptedString",
match: UriMatchType.Domain,
},
],
totp: "EncryptedString",
autofillOnPageLoad: false,
fido2Credentials: undefined,
},
identity: undefined,
card: undefined,
secureNote: undefined,
sshKey: undefined,
favorite: false,
reprompt: SdkCipherRepromptType.None,
organizationUseTotp: true,
edit: true,
permissions: new CipherPermissionsApi(),
viewPassword: true,
localData: {
lastUsedDate: "2025-04-15T12:00:00.000Z",
lastLaunched: "2025-04-15T12:00:00.000Z",
},
attachments: [
{
id: "a1",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
{
id: "a2",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
],
fields: [
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Linked,
linkedId: LoginLinkedIdType.Username,
},
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Linked,
linkedId: LoginLinkedIdType.Password,
},
],
passwordHistory: [
{
password: "EncryptedString",
lastUsedDate: "2022-01-31T12:00:00.000Z",
},
],
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: undefined,
revisionDate: "2022-01-31T12:00:00.000Z",
};
const lastUsedDate = new Date("2025-04-15T12:00:00.000Z").getTime();
const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime();
const cipherData: CipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
permissions: new CipherPermissionsApi(),
collectionIds: [],
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Login,
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
reprompt: CipherRepromptType.None,
key: "EncryptedString",
login: {
uris: [
{
uri: "EncryptedString",
uriChecksum: "EncryptedString",
match: UriMatchStrategy.Domain,
},
],
username: "EncryptedString",
password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "EncryptedString",
autofillOnPageLoad: false,
},
passwordHistory: [
{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
],
attachments: [
{
id: "a1",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
{
id: "a2",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "file",
key: "EncKey",
},
],
fields: [
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Linked,
linkedId: LoginLinkedId.Username,
},
{
name: "EncryptedString",
value: "EncryptedString",
type: FieldType.Linked,
linkedId: LoginLinkedId.Password,
},
],
};
const expectedCipher = new Cipher(cipherData, { lastUsedDate, lastLaunched });
const cipher = Cipher.fromSdkCipher(sdkCipher);
expect(cipher).toEqual(expectedCipher);
});
});
});

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { uuidToString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
import { EncString } from "../../../key-management/crypto/models/enc-string";
@@ -15,7 +16,7 @@ import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherPermissionsApi } from "../api/cipher-permissions.api";
import { CipherData } from "../data/cipher.data";
import { LocalData } from "../data/local.data";
import { LocalData, fromSdkLocalData, toSdkLocalData } from "../data/local.data";
import { AttachmentView } from "../view/attachment.view";
import { CipherView } from "../view/cipher.view";
import { FieldView } from "../view/field.view";
@@ -362,16 +363,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
}
: undefined,
viewPassword: this.viewPassword ?? true,
localData: this.localData
? {
lastUsedDate: this.localData.lastUsedDate
? new Date(this.localData.lastUsedDate).toISOString()
: undefined,
lastLaunched: this.localData.lastLaunched
? new Date(this.localData.lastLaunched).toISOString()
: undefined,
}
: undefined,
localData: toSdkLocalData(this.localData),
attachments: this.attachments?.map((a) => a.toSdkAttachment()),
fields: this.fields?.map((f) => f.toSdkField()),
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()),
@@ -409,4 +401,50 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
return sdkCipher;
}
/**
* Maps an SDK Cipher object to a Cipher
* @param sdkCipher - The SDK Cipher object
*/
static fromSdkCipher(sdkCipher: SdkCipher | null): Cipher | undefined {
if (sdkCipher == null) {
return undefined;
}
const cipher = new Cipher();
cipher.id = sdkCipher.id ? uuidToString(sdkCipher.id) : undefined;
cipher.organizationId = sdkCipher.organizationId
? uuidToString(sdkCipher.organizationId)
: undefined;
cipher.folderId = sdkCipher.folderId ? uuidToString(sdkCipher.folderId) : undefined;
cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidToString) : [];
cipher.key = EncString.fromJSON(sdkCipher.key);
cipher.name = EncString.fromJSON(sdkCipher.name);
cipher.notes = EncString.fromJSON(sdkCipher.notes);
cipher.type = sdkCipher.type;
cipher.favorite = sdkCipher.favorite;
cipher.organizationUseTotp = sdkCipher.organizationUseTotp;
cipher.edit = sdkCipher.edit;
cipher.permissions = CipherPermissionsApi.fromSdkCipherPermissions(sdkCipher.permissions);
cipher.viewPassword = sdkCipher.viewPassword;
cipher.localData = fromSdkLocalData(sdkCipher.localData);
cipher.attachments = sdkCipher.attachments?.map((a) => Attachment.fromSdkAttachment(a)) ?? [];
cipher.fields = sdkCipher.fields?.map((f) => Field.fromSdkField(f)) ?? [];
cipher.passwordHistory =
sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? [];
cipher.creationDate = new Date(sdkCipher.creationDate);
cipher.revisionDate = new Date(sdkCipher.revisionDate);
cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : null;
cipher.reprompt = sdkCipher.reprompt;
// Cipher type specific properties
cipher.login = Login.fromSdkLogin(sdkCipher.login);
cipher.secureNote = SecureNote.fromSdkSecureNote(sdkCipher.secureNote);
cipher.card = Card.fromSdkCard(sdkCipher.card);
cipher.identity = Identity.fromSdkIdentity(sdkCipher.identity);
cipher.sshKey = SshKey.fromSdkSshKey(sdkCipher.sshKey);
return cipher;
}
}

View File

@@ -173,4 +173,32 @@ export class Fido2Credential extends Domain {
creationDate: this.creationDate.toISOString(),
};
}
/**
* Maps an SDK Fido2Credential object to a Fido2Credential
* @param obj - The SDK Fido2Credential object
*/
static fromSdkFido2Credential(obj: SdkFido2Credential): Fido2Credential | undefined {
if (!obj) {
return undefined;
}
const credential = new Fido2Credential();
credential.credentialId = EncString.fromJSON(obj.credentialId);
credential.keyType = EncString.fromJSON(obj.keyType);
credential.keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm);
credential.keyCurve = EncString.fromJSON(obj.keyCurve);
credential.keyValue = EncString.fromJSON(obj.keyValue);
credential.rpId = EncString.fromJSON(obj.rpId);
credential.userHandle = EncString.fromJSON(obj.userHandle);
credential.userName = EncString.fromJSON(obj.userName);
credential.counter = EncString.fromJSON(obj.counter);
credential.rpName = EncString.fromJSON(obj.rpName);
credential.userDisplayName = EncString.fromJSON(obj.userDisplayName);
credential.discoverable = EncString.fromJSON(obj.discoverable);
credential.creationDate = new Date(obj.creationDate);
return credential;
}
}

View File

@@ -1,6 +1,14 @@
import {
Field as SdkField,
FieldType,
LoginLinkedIdType,
CardLinkedIdType,
IdentityLinkedIdType,
} from "@bitwarden/sdk-internal";
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums";
import { CardLinkedId, IdentityLinkedId, LoginLinkedId } from "../../enums";
import { FieldData } from "../../models/data/field.data";
import { Field } from "../../models/domain/field";
@@ -103,5 +111,34 @@ describe("Field", () => {
identityField.linkedId = IdentityLinkedId.LicenseNumber;
expect(identityField.toSdkField().linkedId).toBe(415);
});
it("should map from SDK Field", () => {
// Test Login LinkedId
const loginField: SdkField = {
name: undefined,
value: undefined,
type: FieldType.Linked,
linkedId: LoginLinkedIdType.Username,
};
expect(Field.fromSdkField(loginField)!.linkedId).toBe(100);
// Test Card LinkedId
const cardField: SdkField = {
name: undefined,
value: undefined,
type: FieldType.Linked,
linkedId: CardLinkedIdType.Number,
};
expect(Field.fromSdkField(cardField)!.linkedId).toBe(305);
// Test Identity LinkedId
const identityFieldSdkField: SdkField = {
name: undefined,
value: undefined,
type: FieldType.Linked,
linkedId: IdentityLinkedIdType.LicenseNumber,
};
expect(Field.fromSdkField(identityFieldSdkField)!.linkedId).toBe(415);
});
});
});

View File

@@ -90,4 +90,22 @@ export class Field extends Domain {
linkedId: this.linkedId as unknown as SdkLinkedIdType,
};
}
/**
* Maps SDK Field to Field
* @param obj The SDK Field object to map
*/
static fromSdkField(obj: SdkField): Field | undefined {
if (!obj) {
return undefined;
}
const field = new Field();
field.name = EncString.fromJSON(obj.name);
field.value = EncString.fromJSON(obj.value);
field.type = obj.type;
field.linkedId = obj.linkedId;
return field;
}
}

View File

@@ -195,4 +195,36 @@ export class Identity extends Domain {
licenseNumber: this.licenseNumber?.toJSON(),
};
}
/**
* Maps an SDK Identity object to an Identity
* @param obj - The SDK Identity object
*/
static fromSdkIdentity(obj: SdkIdentity): Identity | undefined {
if (obj == null) {
return undefined;
}
const identity = new Identity();
identity.title = EncString.fromJSON(obj.title);
identity.firstName = EncString.fromJSON(obj.firstName);
identity.middleName = EncString.fromJSON(obj.middleName);
identity.lastName = EncString.fromJSON(obj.lastName);
identity.address1 = EncString.fromJSON(obj.address1);
identity.address2 = EncString.fromJSON(obj.address2);
identity.address3 = EncString.fromJSON(obj.address3);
identity.city = EncString.fromJSON(obj.city);
identity.state = EncString.fromJSON(obj.state);
identity.postalCode = EncString.fromJSON(obj.postalCode);
identity.country = EncString.fromJSON(obj.country);
identity.company = EncString.fromJSON(obj.company);
identity.email = EncString.fromJSON(obj.email);
identity.phone = EncString.fromJSON(obj.phone);
identity.ssn = EncString.fromJSON(obj.ssn);
identity.username = EncString.fromJSON(obj.username);
identity.passportNumber = EncString.fromJSON(obj.passportNumber);
identity.licenseNumber = EncString.fromJSON(obj.licenseNumber);
return identity;
}
}

View File

@@ -102,4 +102,17 @@ export class LoginUri extends Domain {
match: this.match,
};
}
static fromSdkLoginUri(obj: SdkLoginUri): LoginUri | undefined {
if (obj == null) {
return undefined;
}
const view = new LoginUri();
view.uri = EncString.fromJSON(obj.uri);
view.uriChecksum = obj.uriChecksum ? EncString.fromJSON(obj.uriChecksum) : undefined;
view.match = obj.match;
return view;
}
}

View File

@@ -163,4 +163,31 @@ export class Login extends Domain {
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
};
}
/**
* Maps an SDK Login object to a Login
* @param obj - The SDK Login object
*/
static fromSdkLogin(obj: SdkLogin): Login | undefined {
if (!obj) {
return undefined;
}
const login = new Login();
login.uris =
obj.uris?.filter((u) => u.uri != null).map((uri) => LoginUri.fromSdkLoginUri(uri)) ?? [];
login.username = EncString.fromJSON(obj.username);
login.password = EncString.fromJSON(obj.password);
login.passwordRevisionDate = obj.passwordRevisionDate
? new Date(obj.passwordRevisionDate)
: undefined;
login.totp = EncString.fromJSON(obj.totp);
login.autofillOnPageLoad = obj.autofillOnPageLoad ?? false;
login.fido2Credentials = obj.fido2Credentials?.map((f) =>
Fido2Credential.fromSdkFido2Credential(f),
);
return login;
}
}

View File

@@ -71,4 +71,20 @@ export class Password extends Domain {
lastUsedDate: this.lastUsedDate.toISOString(),
};
}
/**
* Maps an SDK PasswordHistory object to a Password
* @param obj - The SDK PasswordHistory object
*/
static fromSdkPasswordHistory(obj: PasswordHistory): Password | undefined {
if (!obj) {
return undefined;
}
const passwordHistory = new Password();
passwordHistory.password = EncString.fromJSON(obj.password);
passwordHistory.lastUsedDate = new Date(obj.lastUsedDate);
return passwordHistory;
}
}

View File

@@ -54,4 +54,19 @@ export class SecureNote extends Domain {
type: this.type,
};
}
/**
* Maps an SDK SecureNote object to a SecureNote
* @param obj - The SDK SecureNote object
*/
static fromSdkSecureNote(obj: SdkSecureNote): SecureNote | undefined {
if (obj == null) {
return undefined;
}
const secureNote = new SecureNote();
secureNote.type = obj.type;
return secureNote;
}
}

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