1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 20:50:28 +00:00

Merge remote-tracking branch 'origin' into auth/pm-18720/change-password-component-non-dialog-v2

This commit is contained in:
Patrick Pimentel
2025-05-14 12:40:14 -04:00
132 changed files with 2507 additions and 698 deletions

View File

@@ -222,7 +222,6 @@
"@types/chrome",
"@types/firefox-webext-browser",
"@types/glob",
"@types/jquery",
"@types/lowdb",
"@types/node",
"@types/node-forge",
@@ -330,9 +329,7 @@
"autoprefixer",
"bootstrap",
"chromatic",
"jquery",
"ngx-toastr",
"popper.js",
"react",
"react-dom",
"remark-gfm",

View File

@@ -73,7 +73,7 @@ jobs:
run: npm run build-storybook:ci
- name: Publish to Chromatic
uses: chromaui/action@8a12962215a66cd05b1ac5b0f1c08768d1aab155 # v11.25.0
uses: chromaui/action@e8cc4c31775280b175a3c440076c00d19a9014d7 # v11.28.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@@ -2208,15 +2208,6 @@
"vaultTimeoutAction1": {
"message": "Timeout action"
},
"newCustomizationOptionsCalloutTitle": {
"message": "New customization options"
},
"newCustomizationOptionsCalloutContent": {
"message": "Customize your vault experience with quick copy actions, compact mode, and more!"
},
"newCustomizationOptionsCalloutLink": {
"message": "View all Appearance settings"
},
"lock": {
"message": "Lock",
"description": "Verb form: to make secure or inaccessible by"
@@ -5273,8 +5264,14 @@
"hasItemsVaultNudgeTitle": {
"message": "Welcome to your vault!"
},
"hasItemsVaultNudgeBody": {
"message": "Autofill items for the current page\nFavorite items for easy access\nSearch your vault for something else"
"hasItemsVaultNudgeBodyOne": {
"message": "Autofill items for the current page"
},
"hasItemsVaultNudgeBodyTwo": {
"message": "Favorite items for easy access"
},
"hasItemsVaultNudgeBodyThree": {
"message": "Search your vault for something else"
},
"newLoginNudgeTitle": {
"message": "Save time with autofill"

View File

@@ -894,9 +894,7 @@ export default class NotificationBackground {
private async getDecryptedCipherById(cipherId: string, userId: UserId) {
const cipher = await this.cipherService.get(cipherId, userId);
if (cipher != null && cipher.type === CipherType.Login) {
return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
);
return await this.cipherService.decrypt(cipher, userId);
}
return null;
}

View File

@@ -48,6 +48,7 @@ export function NotificationConfirmationBody({
? NotificationConfirmationMessage({
buttonAria,
buttonText,
error,
itemName,
message: confirmationMessage,
messageDetails,

View File

@@ -8,6 +8,7 @@ import { spacing, themes, typography } from "../../constants/styles";
export type NotificationConfirmationMessageProps = {
buttonAria?: string;
buttonText?: string;
error?: string;
itemName?: string;
message?: string;
messageDetails?: string;
@@ -18,6 +19,7 @@ export type NotificationConfirmationMessageProps = {
export function NotificationConfirmationMessage({
buttonAria,
buttonText,
error,
itemName,
message,
messageDetails,
@@ -29,7 +31,11 @@ export function NotificationConfirmationMessage({
${message || buttonText
? html`
<div class=${singleLineWrapperStyles}>
<span class=${itemNameStyles(theme)} title=${itemName}> ${itemName} </span>
${!error && itemName
? html`
<span class=${itemNameStyles(theme)} title=${itemName}> ${itemName} </span>
`
: nothing}
<span
title=${message || buttonText}
class=${notificationConfirmationMessageStyles(theme)}

View File

@@ -17,7 +17,7 @@ export class OverlayNotificationsContentService
private notificationBarIframeElement: HTMLIFrameElement | null = null;
private currentNotificationBarType: string | null = null;
private removeTabFromNotificationQueueTypes = new Set(["add", "change"]);
private notificationRefreshFlag: boolean;
private notificationRefreshFlag: boolean = false;
private notificationBarElementStyles: Partial<CSSStyleDeclaration> = {
height: "82px",
width: "430px",
@@ -57,6 +57,7 @@ export class OverlayNotificationsContentService
void sendExtensionMessage("checkNotificationQueue");
void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => {
this.notificationRefreshFlag = !!notificationRefreshFlag;
this.setNotificationRefreshBarHeight();
});
}
@@ -223,15 +224,31 @@ export class OverlayNotificationsContentService
this.notificationBarElement.id = "bit-notification-bar";
setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true);
if (this.notificationRefreshFlag) {
setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true);
}
this.setNotificationRefreshBarHeight();
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
}
}
/**
* Sets the height of the notification bar based on the value of `notificationRefreshFlag`.
* If the flag is `true`, the bar is expanded to 400px and aligned right.
* If the flag is `false`, `null`, or `undefined`, it defaults to height of 82px.
* Skips if the notification bar element has not yet been created.
*
*/
private setNotificationRefreshBarHeight() {
const isNotificationV3 = !!this.notificationRefreshFlag;
if (!this.notificationBarElement) {
return;
}
if (isNotificationV3) {
setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true);
}
}
/**
* Sets up the message listener for the initialization of the notification bar.
* This will send the initialization data to the notification bar iframe.

View File

@@ -216,9 +216,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
return this.cipherService.decrypt(cipher, activeUserId);
}),
);
@@ -237,9 +235,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
return this.cipherService.decrypt(cipher, activeUserId);
}),
);

View File

@@ -183,6 +183,7 @@ import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-st
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@@ -199,6 +200,7 @@ import {
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
@@ -408,6 +410,7 @@ export default class MainBackground {
endUserNotificationService: EndUserNotificationService;
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
taskService: TaskService;
cipherEncryptionService: CipherEncryptionService;
ipcContentScriptManagerService: IpcContentScriptManagerService;
ipcService: IpcService;
@@ -856,6 +859,11 @@ export default class MainBackground {
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
this.cipherEncryptionService = new DefaultCipherEncryptionService(
this.sdkService,
this.logService,
);
this.cipherService = new CipherService(
this.keyService,
this.domainSettingsService,
@@ -871,6 +879,7 @@ export default class MainBackground {
this.stateProvider,
this.accountService,
this.logService,
this.cipherEncryptionService,
);
this.folderService = new FolderService(
this.keyService,

View File

@@ -11,7 +11,6 @@ import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -66,11 +65,7 @@ export class AssignCollections {
route.queryParams.pipe(
switchMap(async ({ cipherId }) => {
const cipherDomain = await this.cipherService.get(cipherId, userId);
const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption(
cipherDomain,
userId,
);
return cipherDomain.decrypt(key);
return await this.cipherService.decrypt(cipherDomain, userId);
}),
),
),

View File

@@ -81,6 +81,7 @@ describe("OpenAttachmentsComponent", () => {
useValue: {
get: getCipher,
getKeyForCipherKeyDecryption: () => Promise.resolve(null),
decrypt: jest.fn().mockResolvedValue(cipherView),
},
},
{

View File

@@ -81,9 +81,7 @@ export class OpenAttachmentsComponent implements OnInit {
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
const cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
const cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
if (!cipher.organizationId) {
this.cipherIsAPartOfFreeOrg = false;

View File

@@ -1,29 +0,0 @@
<ng-container *ngIf="showNewCustomizationSettingsCallout">
<button
type="button"
class="tw-absolute tw-bottom-[12px] tw-right-[47px]"
[bitPopoverTriggerFor]="newCustomizationOptionsCallout"
[position]="'above-end'"
[popoverOpen]="true"
#triggerRef="popoverTrigger"
></button>
<bit-popover
[title]="'newCustomizationOptionsCalloutTitle' | i18n"
#newCustomizationOptionsCallout
(closed)="dismissCallout()"
>
<div bitTypography="body2" (click)="goToAppearance()">
{{ "newCustomizationOptionsCalloutContent" | i18n }}
<a
tabIndex="0"
bitLink
class="tw-font-bold"
linkType="primary"
routerLink="/appearance"
(keydown.enter)="goToAppearance()"
>
{{ "newCustomizationOptionsCalloutLink" | i18n }}
</a>
</div>
</bit-popover>
</ng-container>

View File

@@ -1,81 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { ButtonModule, PopoverModule } from "@bitwarden/components";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
import { VaultPageService } from "../vault-page.service";
@Component({
selector: "new-settings-callout",
templateUrl: "new-settings-callout.component.html",
standalone: true,
imports: [PopoverModule, JslibModule, CommonModule, ButtonModule],
providers: [VaultPageService],
})
export class NewSettingsCalloutComponent implements OnInit, OnDestroy {
protected showNewCustomizationSettingsCallout = false;
protected activeUserId: UserId | null = null;
constructor(
private accountService: AccountService,
private vaultProfileService: VaultProfileService,
private vaultPageService: VaultPageService,
private router: Router,
private logService: LogService,
private copyButtonService: VaultPopupCopyButtonsService,
private vaultSettingsService: VaultSettingsService,
) {}
async ngOnInit() {
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const showQuickCopyActions = await firstValueFrom(this.copyButtonService.showQuickCopyActions$);
const clickItemsToAutofillVaultView = await firstValueFrom(
this.vaultSettingsService.clickItemsToAutofillVaultView$,
);
let profileCreatedDate: Date;
try {
profileCreatedDate = await this.vaultProfileService.getProfileCreationDate(this.activeUserId);
} catch (e) {
this.logService.error("Error getting profile creation date", e);
// Default to before the cutoff date to ensure the callout is shown
profileCreatedDate = new Date("2024-12-24");
}
const hasCalloutBeenDismissed = await firstValueFrom(
this.vaultPageService.isCalloutDismissed(this.activeUserId),
);
this.showNewCustomizationSettingsCallout =
!showQuickCopyActions &&
!clickItemsToAutofillVaultView &&
!hasCalloutBeenDismissed &&
profileCreatedDate < new Date("2024-12-25");
}
async goToAppearance() {
await this.router.navigate(["/appearance"]);
}
async dismissCallout() {
if (this.activeUserId) {
await this.vaultPageService.dismissCallout(this.activeUserId);
}
}
async ngOnDestroy() {
await this.dismissCallout();
}
}

View File

@@ -1,35 +0,0 @@
import { inject, Injectable } from "@angular/core";
import { map, Observable } from "rxjs";
import {
BANNERS_DISMISSED_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
export const NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY = new UserKeyDefinition<boolean>(
BANNERS_DISMISSED_DISK,
"newCustomizationOptionsCalloutDismissed",
{
deserializer: (calloutDismissed) => calloutDismissed,
clearOn: [], // Do not clear dismissed callouts
},
);
@Injectable()
export class VaultPageService {
private stateProvider = inject(StateProvider);
isCalloutDismissed(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.state$.pipe(map((dismissed) => !!dismissed));
}
async dismissCallout(userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.update(() => true);
}
}

View File

@@ -69,8 +69,6 @@ export class PasswordHistoryV2Component implements OnInit {
const activeUserId = activeAccount.id as UserId;
const cipher = await this.cipherService.get(cipherId, activeUserId);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
}
}

View File

@@ -44,9 +44,13 @@
<div class="tw-mb-4" *ngIf="showHasItemsVaultSpotlight$ | async">
<bit-spotlight
[title]="'hasItemsVaultNudgeTitle' | i18n"
[subtitle]="'hasItemsVaultNudgeBody' | i18n"
(onDismiss)="dismissVaultNudgeSpotlight(VaultNudgeType.HasVaultItems)"
>
<ul class="tw-pl-4 tw-text-main" bitTypography="body2">
<li>{{ "hasItemsVaultNudgeBodyOne" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyTwo" | i18n }}</li>
<li>{{ "hasItemsVaultNudgeBodyThree" | i18n }}</li>
</ul>
</bit-spotlight>
</div>
<vault-at-risk-password-callout
@@ -103,5 +107,4 @@
></app-vault-list-items-container>
</div>
</ng-container>
<new-settings-callout></new-settings-callout>
</popup-page>

View File

@@ -19,10 +19,17 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
import {
ButtonModule,
DialogService,
Icons,
NoItemsModule,
TypographyModule,
} from "@bitwarden/components";
import {
DecryptionFailureDialogComponent,
SpotlightComponent,
@@ -49,9 +56,7 @@ import {
NewItemDropdownV2Component,
NewItemInitialValues,
} from "./new-item-dropdown/new-item-dropdown-v2.component";
import { NewSettingsCalloutComponent } from "./new-settings-callout/new-settings-callout.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { VaultPageService } from "./vault-page.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
@@ -83,11 +88,10 @@ enum VaultState {
ScrollingModule,
VaultHeaderV2Component,
AtRiskPasswordCalloutComponent,
NewSettingsCalloutComponent,
SpotlightComponent,
RouterModule,
TypographyModule,
],
providers: [VaultPageService],
})
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
@@ -144,7 +148,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
protected noResultsIcon = Icons.NoResults;
protected VaultStateEnum = VaultState;
protected showNewCustomizationSettingsCallout = false;
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
@@ -158,6 +161,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private introCarouselService: IntroCarouselService,
private vaultNudgesService: VaultNudgesService,
private router: Router,
private i18nService: I18nService,
) {
combineLatest([
this.vaultPopupItemsService.emptyVault$,

View File

@@ -82,6 +82,7 @@ describe("ViewV2Component", () => {
getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}),
deleteWithServer: jest.fn().mockResolvedValue(undefined),
softDeleteWithServer: jest.fn().mockResolvedValue(undefined),
decrypt: jest.fn().mockResolvedValue(mockCipher),
};
beforeEach(async () => {

View File

@@ -203,9 +203,7 @@ export class ViewV2Component {
async getCipherData(id: string, userId: UserId) {
const cipher = await this.cipherService.get(id, userId);
return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
);
return await this.cipherService.decrypt(cipher, userId);
}
async editCipher() {

View File

@@ -59,15 +59,11 @@ export class ShareCommand {
return Response.badRequest("This item already belongs to an organization.");
}
const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const cipherView = await this.cipherService.decrypt(cipher, activeUserId);
try {
await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId);
const updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {

View File

@@ -90,9 +90,7 @@ export class EditCommand {
return Response.notFound();
}
let cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
let cipherView = await this.cipherService.decrypt(cipher, activeUserId);
if (cipherView.isDeleted) {
return Response.badRequest("You may not edit a deleted item. Use the restore command first.");
}
@@ -100,9 +98,7 @@ export class EditCommand {
const encCipher = await this.cipherService.encrypt(cipherView, activeUserId);
try {
const updatedCipher = await this.cipherService.updateWithServer(encCipher);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
@@ -132,12 +128,7 @@ export class EditCommand {
cipher,
activeUserId,
);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(
updatedCipher,
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {

View File

@@ -116,9 +116,7 @@ export class GetCommand extends DownloadCommand {
if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id, activeUserId);
if (cipher != null) {
decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
decCipher = await this.cipherService.decrypt(cipher, activeUserId);
}
} else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted(activeUserId);

View File

@@ -139,12 +139,14 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider";
import { SendService } from "@bitwarden/common/tools/send/services/send.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import {
CipherAuthorizationService,
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
@@ -284,6 +286,7 @@ export class ServiceContainer {
ssoUrlService: SsoUrlService;
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
bulkEncryptService: FallbackBulkEncryptService;
cipherEncryptionService: CipherEncryptionService;
constructor() {
let p = null;
@@ -679,6 +682,11 @@ export class ServiceContainer {
this.accountService,
);
this.cipherEncryptionService = new DefaultCipherEncryptionService(
this.sdkService,
this.logService,
);
this.cipherService = new CipherService(
this.keyService,
this.domainSettingsService,
@@ -694,6 +702,7 @@ export class ServiceContainer {
this.stateProvider,
this.accountService,
this.logService,
this.cipherEncryptionService,
);
this.folderService = new FolderService(

View File

@@ -93,9 +93,7 @@ export class CreateCommand {
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try {
const newCipher = await this.cipherService.createWithServer(cipher);
const decCipher = await newCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(newCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(newCipher, activeUserId);
const res = new CipherResponse(decCipher);
return Response.success(res);
} catch (e) {
@@ -162,9 +160,7 @@ export class CreateCommand {
new Uint8Array(fileBuf).buffer,
activeUserId,
);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(updatedCipher, activeUserId);
return Response.success(new CipherResponse(decCipher));
} catch (e) {
return Response.error(e);

View File

@@ -3479,9 +3479,9 @@ dependencies = [
[[package]]
name = "widestring"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
[[package]]
name = "winapi"
@@ -3568,7 +3568,7 @@ dependencies = [
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
"windows-strings",
]
[[package]]
@@ -3643,13 +3643,13 @@ dependencies = [
[[package]]
name = "windows-registry"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
dependencies = [
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
"windows-strings",
]
[[package]]
@@ -3670,15 +3670,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
@@ -3730,29 +3721,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -3765,12 +3740,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -3783,12 +3752,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -3801,24 +3764,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -3831,12 +3782,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_plugin_authenticator"
version = "0.0.0"
@@ -3858,12 +3803,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -3876,12 +3815,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -3894,12 +3827,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.3"

View File

@@ -56,11 +56,11 @@ tokio-stream = "=0.1.15"
tokio-util = "=0.7.13"
typenum = "=1.18.0"
uniffi = "=0.28.3"
widestring = "=1.1.0"
widestring = "=1.2.0"
windows = "=0.61.1"
windows-core = "=0.61.0"
windows-future = "=0.2.0"
windows-registry = "=0.4.0"
windows-registry = "=0.5.1"
zbus = "=4.4.0"
zbus_polkit = "=4.0.0"
zeroizing-alloc = "=0.1.0"

View File

@@ -199,9 +199,7 @@ export class DesktopAutofillService implements OnDestroy {
return;
}
const decrypted = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const decrypted = await this.cipherService.decrypt(cipher, activeUserId);
const fido2Credential = decrypted.login.fido2Credentials?.[0];
if (!fido2Credential) {

View File

@@ -207,9 +207,7 @@ export class EncryptedMessageHandlerService {
return { status: "failure" };
}
const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const cipherView = await this.cipherService.decrypt(cipher, activeUserId);
cipherView.name = credentialUpdatePayload.name;
cipherView.login.password = credentialUpdatePayload.password;
cipherView.login.username = credentialUpdatePayload.userName;

View File

@@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -33,6 +34,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
toastService: ToastService,
configService: ConfigService,
) {
super(
cipherService,
@@ -49,6 +51,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent {
billingAccountProfileStateService,
accountService,
toastService,
configService,
);
}
}

View File

@@ -72,7 +72,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
accountService: AccountService,
toastService: ToastService,
cipherAuthorizationService: CipherAuthorizationService,
private configService: ConfigService,
configService: ConfigService,
) {
super(
cipherService,
@@ -100,6 +100,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
billingAccountProfileStateService,
toastService,
cipherAuthorizationService,
configService,
);
}

View File

@@ -3,25 +3,20 @@
import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { Router } from "@angular/router";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -29,11 +24,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { PolicyListService } from "./admin-console/core/policy-list.service";
@@ -69,8 +62,6 @@ export class AppComponent implements OnDestroy, OnInit {
@Inject(DOCUMENT) private document: Document,
private broadcasterService: BroadcasterService,
private folderService: InternalFolderService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
@@ -85,17 +76,13 @@ export class AppComponent implements OnDestroy, OnInit {
private notificationsService: NotificationsService,
private stateService: StateService,
private eventUploadService: EventUploadService,
private policyService: InternalPolicyService,
protected policyListService: PolicyListService,
private keyConnectorService: KeyConnectorService,
protected configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,
private organizationService: InternalOrganizationServiceAbstraction,
private accountService: AccountService,
private apiService: ApiService,
private appIdService: AppIdService,
private processReloadService: ProcessReloadServiceAbstraction,
private deviceTrustToastService: DeviceTrustToastService,
) {
@@ -247,15 +234,6 @@ export class AppComponent implements OnDestroy, OnInit {
});
});
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll(".modal"));
for (const modal of modals) {
(jq(modal) as any).modal("hide");
}
}
});
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),

View File

@@ -67,7 +67,9 @@
{{ "resendInvitation" | i18n }}
</button>
<hr class="m-0" />
<ng-container *ngIf="!isSelfHosted && !sponsoredFamily.validUntil">
<hr class="tw-m-0" />
</ng-container>
<button
type="button"

View File

@@ -26,7 +26,6 @@ import {
WINDOW,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import {
RegistrationFinishService as RegistrationFinishServiceAbstraction,
LoginComponentService,
@@ -139,7 +138,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi
import { EventService } from "./event.service";
import { InitService } from "./init.service";
import { ENV_URLS } from "./injection-tokens";
import { ModalService } from "./modal.service";
import { RouterService } from "./router.service";
import { WebPlatformUtilsService } from "./web-platform-utils.service";
@@ -198,11 +196,6 @@ const safeProviders: SafeProvider[] = [
useClass: WebPlatformUtilsService,
useAngularDecorators: true,
}),
safeProvider({
provide: ModalServiceAbstraction,
useClass: ModalService,
useAngularDecorators: true,
}),
safeProvider({
provide: FileDownloadService,
useClass: WebFileDownloadService,

View File

@@ -1,56 +0,0 @@
import { Injectable, Injector } from "@angular/core";
import * as jq from "jquery";
import { first } from "rxjs/operators";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService as BaseModalService } from "@bitwarden/angular/services/modal.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@Injectable()
export class ModalService extends BaseModalService {
el: any = null;
modalOpen = false;
constructor(
injector: Injector,
private messagingService: MessagingService,
) {
super(injector);
}
protected setupHandlers(modalRef: ModalRef) {
modalRef.onCreated.pipe(first()).subscribe(() => {
const modals = Array.from(document.querySelectorAll(".modal"));
if (modals.length > 0) {
this.el = jq(modals[0]);
this.el.modal("show");
this.el.on("show.bs.modal", () => {
modalRef.show();
this.messagingService.send("modalShow");
});
this.el.on("shown.bs.modal", () => {
modalRef.shown();
this.messagingService.send("modalShown");
if (!Utils.isMobileBrowser) {
this.el.find("*[appAutoFocus]").focus();
}
});
this.el.on("hide.bs.modal", () => {
this.messagingService.send("modalClose");
});
this.el.on("hidden.bs.modal", () => {
modalRef.closed();
this.messagingService.send("modalClosed");
});
}
});
modalRef.onClose.pipe(first()).subscribe(() => {
if (this.el != null) {
this.el.modal("hide");
}
});
}
}

View File

@@ -481,9 +481,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
activeUserId,
);
updatedCipherView = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
updatedCipherView = await this.cipherService.decrypt(updatedCipher, activeUserId);
}
this.cipherFormComponent.patchCipher((currentCipher) => {
@@ -520,9 +518,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
return;
}
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return await config.originalCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId),
);
return await this.cipherService.decrypt(config.originalCipher, activeUserId);
}
private updateTitle() {

View File

@@ -140,9 +140,15 @@
"atRiskMembersDescription": {
"message": "These members are logging into applications with weak, exposed, or reused passwords."
},
"atRiskMembersDescriptionNone": {
"message": "These are no members logging into applications with weak, exposed, or reused passwords."
},
"atRiskApplicationsDescription": {
"message": "These applications have weak, exposed, or reused passwords."
},
"atRiskApplicationsDescriptionNone": {
"message": "These are no applications with weak, exposed, or reused passwords."
},
"atRiskMembersDescriptionWithApp": {
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
"placeholders": {
@@ -152,6 +158,15 @@
}
}
},
"atRiskMembersDescriptionWithAppNone": {
"message": "There are no at risk members for $APPNAME$.",
"placeholders": {
"appname": {
"content": "$1",
"example": "Salesforce"
}
}
},
"totalMembers": {
"message": "Total members"
},

View File

@@ -1,10 +1,6 @@
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import "bootstrap";
import "jquery";
import "popper.js";
import { AppModule } from "./app/app.module";
if (process.env.NODE_ENV === "production") {

View File

@@ -89,7 +89,7 @@
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="unmarkAsCriticalApp(row.applicationName)">
<i aria-hidden="true" class="bwi bwi-star-f"></i> {{ "unmarkAsCriticalApp" | i18n }}
{{ "unmarkAsCriticalApp" | i18n }}
</button>
</bit-menu>
</td>

View File

@@ -68,19 +68,24 @@
</bit-drawer-header>
<bit-drawer-body>
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
"atRiskMembersDescription" | i18n
(dataService.atRiskMemberDetails.length > 0
? "atRiskMembersDescription"
: "atRiskMembersDescriptionNone"
) | i18n
}}</span>
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">{{ "email" | i18n }}</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</div>
<ng-container *ngFor="let member of dataService.atRiskMemberDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ member.email }}</div>
<div>{{ member.atRiskPasswordCount }}</div>
<ng-container *ngIf="dataService.atRiskMemberDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">{{ "email" | i18n }}</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</div>
<ng-container *ngFor="let member of dataService.atRiskMemberDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ member.email }}</div>
<div>{{ member.atRiskPasswordCount }}</div>
</div>
</ng-container>
</ng-container>
</bit-drawer-body>
</ng-container>
@@ -94,7 +99,10 @@
</div>
<div bitTypography="body1" class="tw-text-muted tw-text-sm tw-mb-2">
{{
"atRiskMembersDescriptionWithApp" | i18n: dataService.appAtRiskMembers.applicationName
(dataService.appAtRiskMembers.members.length > 0
? "atRiskMembersDescriptionWithApp"
: "atRiskMembersDescriptionWithAppNone"
) | i18n: dataService.appAtRiskMembers.applicationName
}}
</div>
<div class="tw-mt-1">
@@ -113,21 +121,26 @@
<bit-drawer-body>
<span bitTypography="body2" class="tw-text-muted tw-text-sm">{{
"atRiskApplicationsDescription" | i18n
(dataService.atRiskAppDetails.length > 0
? "atRiskApplicationsDescription"
: "atRiskApplicationsDescriptionNone"
) | i18n
}}</span>
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "application" | i18n }}
</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</div>
<ng-container *ngFor="let app of dataService.atRiskAppDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ app.applicationName }}</div>
<div>{{ app.atRiskPasswordCount }}</div>
<ng-container *ngIf="dataService.atRiskAppDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "application" | i18n }}
</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</div>
<ng-container *ngFor="let app of dataService.atRiskAppDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ app.applicationName }}</div>
<div>{{ app.atRiskPasswordCount }}</div>
</div>
</ng-container>
</ng-container>
</bit-drawer-body>
</ng-container>

View File

@@ -1,10 +1,6 @@
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import "bootstrap";
import "jquery";
import "popper.js";
import { AppModule } from "./app/app.module";
if (process.env.NODE_ENV === "production") {

View File

@@ -50,9 +50,7 @@ export class CollectionsComponent implements OnInit {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.cipherDomain = await this.loadCipher(activeUserId);
this.collectionIds = this.loadCipherCollections();
this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
);
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
this.collections = await this.loadCollections();
this.collections.forEach((c) => ((c as any).checked = false));

View File

@@ -76,9 +76,7 @@ export class ShareComponent implements OnInit, OnDestroy {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
this.cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
this.cipher = await this.cipherService.decrypt(cipherDomain, activeUserId);
}
filterCollections() {
@@ -105,9 +103,7 @@ export class ShareComponent implements OnInit, OnDestroy {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
const cipherView = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
const cipherView = await this.cipherService.decrypt(cipherDomain, activeUserId);
const orgs = await firstValueFrom(this.organizations$);
const orgName =
orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization");

View File

@@ -265,6 +265,7 @@ import {
InternalSendService,
SendService as SendServiceAbstraction,
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@@ -283,6 +284,7 @@ import {
DefaultCipherAuthorizationService,
} from "@bitwarden/common/vault/services/cipher-authorization.service";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
@@ -511,6 +513,7 @@ const safeProviders: SafeProvider[] = [
stateProvider: StateProvider,
accountService: AccountServiceAbstraction,
logService: LogService,
cipherEncryptionService: CipherEncryptionService,
) =>
new CipherService(
keyService,
@@ -527,6 +530,7 @@ const safeProviders: SafeProvider[] = [
stateProvider,
accountService,
logService,
cipherEncryptionService,
),
deps: [
KeyService,
@@ -543,6 +547,7 @@ const safeProviders: SafeProvider[] = [
StateProvider,
AccountServiceAbstraction,
LogService,
CipherEncryptionService,
],
}),
safeProvider({
@@ -1530,6 +1535,11 @@ const safeProviders: SafeProvider[] = [
useClass: MasterPasswordApiService,
deps: [ApiServiceAbstraction, LogService],
}),
safeProvider({
provide: CipherEncryptionService,
useClass: DefaultCipherEncryptionService,
deps: [SdkService, LogService],
}),
safeProvider({
provide: ChangePasswordService,
useClass: DefaultChangePasswordService,

View File

@@ -269,9 +269,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
if (this.cipher == null) {
if (this.editMode) {
const cipher = await this.loadCipher(activeUserId);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
// Adjust Cipher Name if Cloning
if (this.cloneMode) {

View File

@@ -9,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -56,6 +56,7 @@ export class AttachmentsComponent implements OnInit {
protected billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
protected toastService: ToastService,
protected configService: ConfigService,
) {}
async ngOnInit() {
@@ -88,9 +89,7 @@ export class AttachmentsComponent implements OnInit {
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
this.cipherDomain = await this.formPromise;
this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
);
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
this.toastService.showToast({
variant: "success",
title: null,
@@ -130,9 +129,7 @@ export class AttachmentsComponent implements OnInit {
const updatedCipher = await this.deletePromises[attachment.id];
const cipher = new Cipher(updatedCipher);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
this.cipher = await this.cipherService.decrypt(cipher, activeUserId);
this.toastService.showToast({
variant: "success",
@@ -197,12 +194,14 @@ export class AttachmentsComponent implements OnInit {
}
try {
const encBuf = await EncArrayBuffer.fromResponse(response);
const key =
attachment.key != null
? attachment.key
: await this.keyService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
this.cipherDomain.id as CipherId,
attachment,
response,
activeUserId,
);
this.fileDownloadService.download({
fileName: attachment.fileName,
blobData: decBuf,
@@ -228,9 +227,7 @@ export class AttachmentsComponent implements OnInit {
protected async init() {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.cipherDomain = await this.loadCipher(activeUserId);
this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
);
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
const canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
@@ -276,15 +273,17 @@ export class AttachmentsComponent implements OnInit {
try {
// 2. Resave
const encBuf = await EncArrayBuffer.fromResponse(response);
const key =
attachment.key != null
? attachment.key
: await this.keyService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getUserId),
);
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
this.cipherDomain.id as CipherId,
attachment,
response,
activeUserId,
);
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
this.cipherDomain,
attachment.fileName,
@@ -292,9 +291,7 @@ export class AttachmentsComponent implements OnInit {
activeUserId,
admin,
);
this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
);
this.cipher = await this.cipherService.decrypt(this.cipherDomain, activeUserId);
// 3. Delete old
this.deletePromises[attachment.id] = this.deleteCipherAttachment(

View File

@@ -42,9 +42,7 @@ export class PasswordHistoryComponent implements OnInit {
protected async init() {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(this.cipherId, activeUserId);
const decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
const decCipher = await this.cipherService.decrypt(cipher, activeUserId);
this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory;
}
}

View File

@@ -34,13 +34,13 @@ import { EventType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@@ -137,6 +137,7 @@ export class ViewComponent implements OnDestroy, OnInit {
private billingAccountProfileStateService: BillingAccountProfileStateService,
protected toastService: ToastService,
private cipherAuthorizationService: CipherAuthorizationService,
protected configService: ConfigService,
) {}
ngOnInit() {
@@ -458,19 +459,19 @@ export class ViewComponent implements OnDestroy, OnInit {
}
try {
const encBuf = await EncArrayBuffer.fromResponse(response);
const key =
attachment.key != null
? attachment.key
: await this.keyService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.encryptService.decryptFileData(encBuf, key);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const decBuf = await this.cipherService.getDecryptedAttachmentBuffer(
this.cipher.id as CipherId,
attachment,
response,
activeUserId,
);
this.fileDownloadService.download({
fileName: attachment.fileName,
blobData: decBuf,
});
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
} catch {
this.toastService.showToast({
variant: "error",
title: null,

View File

@@ -64,6 +64,20 @@ export function makeSymmetricCryptoKey<T extends SymmetricCryptoKey>(
*/
export const mockFromJson = (stub: any) => (stub + "_fromJSON") as any;
/**
* Use to mock a return value of a static fromSdk method.
*/
export const mockFromSdk = (stub: any) => {
if (typeof stub === "object") {
return {
...stub,
__fromSdk: true,
};
}
return `${stub}_fromSdk`;
};
/**
* Tracks the emissions of the given observable.
*

View File

@@ -58,6 +58,7 @@ export enum FeatureFlag {
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
SecurityTasks = "security-tasks",
PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk",
CipherKeyEncryption = "cipher-key-encryption",
PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms",
EndUserNotifications = "pm-10609-end-user-notifications",
@@ -112,6 +113,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.CipherKeyEncryption]: FALSE,
[FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE,
[FeatureFlag.EndUserNotifications]: FALSE,
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
/* Auth */
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,

View File

@@ -152,6 +152,7 @@ describe("FidoAuthenticatorService", () => {
id === excludedCipher.id ? ({ decrypt: () => excludedCipher } as any) : undefined,
);
cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]);
cipherService.decrypt.mockResolvedValue(excludedCipher);
});
/**
@@ -220,6 +221,7 @@ describe("FidoAuthenticatorService", () => {
id === existingCipher.id ? ({ decrypt: () => existingCipher } as any) : undefined,
);
cipherService.getAllDecrypted.mockResolvedValue([existingCipher]);
cipherService.decrypt.mockResolvedValue(existingCipher);
});
/**
@@ -306,6 +308,11 @@ describe("FidoAuthenticatorService", () => {
const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password };
cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher);
cipherService.decrypt.mockResolvedValue({
...existingCipher,
reprompt: CipherRepromptType.Password,
} as unknown as CipherView);
const result = async () => await authenticator.makeCredential(params, windowReference);
await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown);
@@ -347,6 +354,7 @@ describe("FidoAuthenticatorService", () => {
cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined,
);
cipherService.getAllDecrypted.mockResolvedValue([await cipher]);
cipherService.decrypt.mockResolvedValue(cipher);
cipherService.encrypt.mockImplementation(async (cipher) => {
cipher.login.fido2Credentials[0].credentialId = credentialId; // Replace id for testability
return {} as any;

View File

@@ -151,9 +151,7 @@ export class Fido2AuthenticatorService<ParentWindowReference>
);
const encrypted = await this.cipherService.get(cipherId, activeUserId);
cipher = await encrypted.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId),
);
cipher = await this.cipherService.decrypt(encrypted, activeUserId);
if (
!userVerified &&

View File

@@ -69,12 +69,13 @@ import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date";
import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed";
import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
export const CURRENT_VERSION = 70;
export const CURRENT_VERSION = 71;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -146,7 +147,8 @@ export function createMigrationBuilder() {
.with(RemoveUnassignedItemsBannerDismissed, 66, 67)
.with(MoveLastSyncDate, 67, 68)
.with(MigrateIncorrectFolderKey, 68, 69)
.with(RemoveAcBannersDismissed, 69, CURRENT_VERSION);
.with(RemoveAcBannersDismissed, 69, 70)
.with(RemoveNewCustomizationOptionsCalloutDismissed, 70, CURRENT_VERSION);
}
export async function currentVersion(

View File

@@ -0,0 +1,50 @@
import { runMigrator } from "../migration-helper.spec";
import { IRREVERSIBLE } from "../migrator";
import { RemoveNewCustomizationOptionsCalloutDismissed } from "./71-remove-new-customization-options-callout-dismissed";
describe("RemoveNewCustomizationOptionsCalloutDismissed", () => {
const sut = new RemoveNewCustomizationOptionsCalloutDismissed(70, 71);
describe("migrate", () => {
it("deletes new customization options callout dismissed from all users", async () => {
const output = await runMigrator(sut, {
global_account_accounts: {
user1: {
email: "user1@email.com",
name: "User 1",
emailVerified: true,
},
user2: {
email: "user2@email.com",
name: "User 2",
emailVerified: true,
},
},
user_user1_bannersDismissed_newCustomizationOptionsCalloutDismissed: true,
user_user2_bannersDismissed_newCustomizationOptionsCalloutDismissed: true,
});
expect(output).toEqual({
global_account_accounts: {
user1: {
email: "user1@email.com",
name: "User 1",
emailVerified: true,
},
user2: {
email: "user2@email.com",
name: "User 2",
emailVerified: true,
},
},
});
});
});
describe("rollback", () => {
it("is irreversible", async () => {
await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE);
});
});
});

View File

@@ -0,0 +1,23 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { IRREVERSIBLE, Migrator } from "../migrator";
export const SHOW_CALLOUT_KEY: KeyDefinitionLike = {
key: "newCustomizationOptionsCalloutDismissed",
stateDefinition: { name: "bannersDismissed" },
};
export class RemoveNewCustomizationOptionsCalloutDismissed extends Migrator<70, 71> {
async migrate(helper: MigrationHelper): Promise<void> {
await Promise.all(
(await helper.getAccounts()).map(async ({ userId }) => {
if (helper.getFromUser(userId, SHOW_CALLOUT_KEY) != null) {
await helper.removeFromUser(userId, SHOW_CALLOUT_KEY);
}
}),
);
}
async rollback(helper: MigrationHelper): Promise<void> {
throw IRREVERSIBLE;
}
}

View File

@@ -0,0 +1,60 @@
import { CipherListView } from "@bitwarden/sdk-internal";
import { UserId } from "../../types/guid";
import { Cipher } from "../models/domain/cipher";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
/**
* Service responsible for encrypting and decrypting ciphers.
*/
export abstract class CipherEncryptionService {
/**
* Decrypts a cipher using the SDK for the given userId.
*
* @param cipher The encrypted cipher object
* @param userId The user ID whose key will be used for decryption
*
* @returns A promise that resolves to the decrypted cipher view
*/
abstract decrypt(cipher: Cipher, userId: UserId): Promise<CipherView>;
/**
* Decrypts many ciphers using the SDK for the given userId.
*
* For bulk decryption, prefer using `decryptMany`, which returns a more efficient
* `CipherListView` object.
*
* @param ciphers The encrypted cipher objects
* @param userId The user ID whose key will be used for decryption
*
* @deprecated Use `decryptMany` for bulk decryption instead.
*
* @returns A promise that resolves to an array of decrypted cipher views
*/
abstract decryptManyLegacy(ciphers: Cipher[], userId: UserId): Promise<CipherView[]>;
/**
* Decrypts many ciphers using the SDK for the given userId.
*
* @param ciphers The encrypted cipher objects
* @param userId The user ID whose key will be used for decryption
*
* @returns A promise that resolves to an array of decrypted cipher list views
*/
abstract decryptMany(ciphers: Cipher[], userId: UserId): Promise<CipherListView[]>;
/**
* Decrypts an attachment's content from a response object.
*
* @param cipher The encrypted cipher object that owns the attachment
* @param attachment The attachment view object
* @param encryptedContent The encrypted content of the attachment
* @param userId The user ID whose key will be used for decryption
*
* @returns A promise that resolves to the decrypted content
*/
abstract decryptAttachmentContent(
cipher: Cipher,
attachment: AttachmentView,
encryptedContent: Uint8Array,
userId: UserId,
): Promise<Uint8Array>;
}

View File

@@ -14,6 +14,7 @@ import { LocalData } from "../models/data/local.data";
import { Cipher } from "../models/domain/cipher";
import { Field } from "../models/domain/field";
import { CipherWithIdRequest } from "../models/request/cipher-with-id.request";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { FieldView } from "../models/view/field.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
@@ -215,4 +216,28 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
): Promise<CipherWithIdRequest[]>;
abstract getNextCardCipher(userId: UserId): Promise<CipherView>;
abstract getNextIdentityCipher(userId: UserId): Promise<CipherView>;
/**
* Decrypts a cipher using either the SDK or the legacy method based on the feature flag.
* @param cipher The cipher to decrypt.
* @param userId The user ID to use for decryption.
* @returns A promise that resolves to the decrypted cipher view.
*/
abstract decrypt(cipher: Cipher, userId: UserId): Promise<CipherView>;
/**
* Decrypts an attachment's content from a response object.
*
* @param cipherId The ID of the cipher that owns the attachment
* @param attachment The attachment view object
* @param response The response object containing the encrypted content
* @param userId The user ID whose key will be used for decryption
*
* @returns A promise that resolves to the decrypted content
*/
abstract getDecryptedAttachmentBuffer(
cipherId: CipherId,
attachment: AttachmentView,
response: Response,
userId: UserId,
): Promise<Uint8Array | null>;
}

View File

@@ -1,5 +1,7 @@
import { Jsonify } from "type-fest";
import { CipherPermissions as SdkCipherPermissions } from "@bitwarden/sdk-internal";
import { BaseResponse } from "../../../models/response/base.response";
export class CipherPermissionsApi extends BaseResponse {
@@ -18,4 +20,19 @@ export class CipherPermissionsApi extends BaseResponse {
static fromJSON(obj: Jsonify<CipherPermissionsApi>) {
return Object.assign(new CipherPermissionsApi(), obj);
}
/**
* Converts the SDK CipherPermissionsApi to a CipherPermissionsApi.
*/
static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined {
if (!obj) {
return undefined;
}
const permissions = new CipherPermissionsApi();
permissions.delete = obj.delete;
permissions.restore = obj.restore;
return permissions;
}
}

View File

@@ -39,7 +39,7 @@ export class CipherData {
passwordHistory?: PasswordHistoryData[];
collectionIds?: string[];
creationDate: string;
deletedDate: string;
deletedDate: string | null;
reprompt: CipherRepromptType;
key: string;

View File

@@ -153,4 +153,21 @@ describe("Attachment", () => {
expect(Attachment.fromJSON(null)).toBeNull();
});
});
describe("toSdkAttachment", () => {
it("should map to SDK Attachment", () => {
const attachment = new Attachment(data);
const sdkAttachment = attachment.toSdkAttachment();
expect(sdkAttachment).toEqual({
id: "id",
url: "url",
size: "1100",
sizeName: "1.1 KB",
fileName: "fileName",
key: "key",
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal";
import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
@@ -113,4 +115,20 @@ export class Attachment extends Domain {
fileName,
});
}
/**
* Maps to SDK Attachment
*
* @returns {SdkAttachment} - The SDK Attachment object
*/
toSdkAttachment(): SdkAttachment {
return {
id: this.id,
url: this.url,
size: this.size,
sizeName: this.sizeName,
fileName: this.fileName?.toJSON(),
key: this.key?.toJSON(),
};
}
}

View File

@@ -99,4 +99,21 @@ describe("Card", () => {
expect(Card.fromJSON(null)).toBeNull();
});
});
describe("toSdkCard", () => {
it("should map to SDK Card", () => {
const card = new Card(data);
const sdkCard = card.toSdkCard();
expect(sdkCard).toEqual({
cardholderName: "encHolder",
brand: "encBrand",
number: "encNumber",
expMonth: "encMonth",
expYear: "encYear",
code: "encCode",
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Card as SdkCard } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -85,4 +87,20 @@ export class Card extends Domain {
code,
});
}
/**
* Maps Card to SDK format.
*
* @returns {SdkCard} The SDK card object.
*/
toSdkCard(): SdkCard {
return {
cardholderName: this.cardholderName?.toJSON(),
brand: this.brand?.toJSON(),
number: this.number?.toJSON(),
expMonth: this.expMonth?.toJSON(),
expYear: this.expYear?.toJSON(),
code: this.code?.toJSON(),
};
}
}

View File

@@ -3,6 +3,12 @@ import { Jsonify } from "type-fest";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { KeyService } from "@bitwarden/key-management";
import {
CipherType as SdkCipherType,
UriMatchType,
CipherRepromptType as SdkCipherRepromptType,
LoginLinkedIdType,
} from "@bitwarden/sdk-internal";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
@@ -12,7 +18,7 @@ import { ContainerService } from "../../../platform/services/container.service";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../abstractions/cipher.service";
import { FieldType, SecureNoteType } from "../../enums";
import { FieldType, LoginLinkedId, SecureNoteType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherData } from "../../models/data/cipher.data";
@@ -770,6 +776,165 @@ describe("Cipher DTO", () => {
expect(Cipher.fromJSON(null)).toBeNull();
});
});
describe("toSdkCipher", () => {
it("should map to SDK Cipher", () => {
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(),
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 cipher = new Cipher(cipherData, { lastUsedDate, lastLaunched });
const sdkCipher = cipher.toSdkCipher();
expect(sdkCipher).toEqual({
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 mockUserId = "TestUserId" as UserId;

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Cipher as SdkCipher } from "@bitwarden/sdk-internal";
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base";
@@ -330,4 +332,72 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
return domain;
}
/**
* Maps Cipher to SDK format.
*
* @returns {SdkCipher} The SDK cipher object.
*/
toSdkCipher(): SdkCipher {
const sdkCipher: SdkCipher = {
id: this.id,
organizationId: this.organizationId,
folderId: this.folderId,
collectionIds: this.collectionIds || [],
key: this.key?.toJSON(),
name: this.name.toJSON(),
notes: this.notes?.toJSON(),
type: this.type,
favorite: this.favorite,
organizationUseTotp: this.organizationUseTotp,
edit: this.edit,
permissions: this.permissions,
viewPassword: this.viewPassword,
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,
attachments: this.attachments?.map((a) => a.toSdkAttachment()),
fields: this.fields?.map((f) => f.toSdkField()),
passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()),
revisionDate: this.revisionDate?.toISOString(),
creationDate: this.creationDate?.toISOString(),
deletedDate: this.deletedDate?.toISOString(),
reprompt: this.reprompt,
// Initialize all cipher-type-specific properties as undefined
login: undefined,
identity: undefined,
card: undefined,
secureNote: undefined,
sshKey: undefined,
};
switch (this.type) {
case CipherType.Login:
sdkCipher.login = this.login.toSdkLogin();
break;
case CipherType.SecureNote:
sdkCipher.secureNote = this.secureNote.toSdkSecureNote();
break;
case CipherType.Card:
sdkCipher.card = this.card.toSdkCard();
break;
case CipherType.Identity:
sdkCipher.identity = this.identity.toSdkIdentity();
break;
case CipherType.SshKey:
sdkCipher.sshKey = this.sshKey.toSdkSshKey();
break;
default:
break;
}
return sdkCipher;
}
}

View File

@@ -167,6 +167,45 @@ describe("Fido2Credential", () => {
expect(Fido2Credential.fromJSON(null)).toBeNull();
});
});
describe("SDK Fido2Credential Mapping", () => {
it("should map to SDK Fido2Credential", () => {
const data: Fido2CredentialData = {
credentialId: "credentialId",
keyType: "public-key",
keyAlgorithm: "ECDSA",
keyCurve: "P-256",
keyValue: "keyValue",
rpId: "rpId",
userHandle: "userHandle",
userName: "userName",
counter: "2",
rpName: "rpName",
userDisplayName: "userDisplayName",
discoverable: "discoverable",
creationDate: mockDate.toISOString(),
};
const credential = new Fido2Credential(data);
const sdkCredential = credential.toSdkFido2Credential();
expect(sdkCredential).toEqual({
credentialId: "credentialId",
keyType: "public-key",
keyAlgorithm: "ECDSA",
keyCurve: "P-256",
keyValue: "keyValue",
rpId: "rpId",
userHandle: "userHandle",
userName: "userName",
counter: "2",
rpName: "rpName",
userDisplayName: "userDisplayName",
discoverable: "discoverable",
creationDate: mockDate.toISOString(),
});
});
});
});
function createEncryptedEncString(s: string): EncString {

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -148,4 +150,27 @@ export class Fido2Credential extends Domain {
creationDate,
});
}
/**
* Maps Fido2Credential to SDK format.
*
* @returns {SdkFido2Credential} The SDK Fido2Credential object.
*/
toSdkFido2Credential(): SdkFido2Credential {
return {
credentialId: this.credentialId?.toJSON(),
keyType: this.keyType.toJSON(),
keyAlgorithm: this.keyAlgorithm.toJSON(),
keyCurve: this.keyCurve.toJSON(),
keyValue: this.keyValue.toJSON(),
rpId: this.rpId.toJSON(),
userHandle: this.userHandle.toJSON(),
userName: this.userName.toJSON(),
counter: this.counter.toJSON(),
rpName: this.rpName?.toJSON(),
userDisplayName: this.userDisplayName?.toJSON(),
discoverable: this.discoverable?.toJSON(),
creationDate: this.creationDate.toISOString(),
};
}
}

View File

@@ -1,6 +1,6 @@
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { FieldType } from "../../enums";
import { CardLinkedId, FieldType, IdentityLinkedId, LoginLinkedId } from "../../enums";
import { FieldData } from "../../models/data/field.data";
import { Field } from "../../models/domain/field";
@@ -82,4 +82,26 @@ describe("Field", () => {
expect(Field.fromJSON(null)).toBeNull();
});
});
describe("SDK Field Mapping", () => {
it("should map to SDK Field", () => {
// Test Login LinkedId
const loginField = new Field(data);
loginField.type = FieldType.Linked;
loginField.linkedId = LoginLinkedId.Username;
expect(loginField.toSdkField().linkedId).toBe(100);
// Test Card LinkedId
const cardField = new Field(data);
cardField.type = FieldType.Linked;
cardField.linkedId = CardLinkedId.Number;
expect(cardField.toSdkField().linkedId).toBe(305);
// Test Identity LinkedId
const identityField = new Field(data);
identityField.type = FieldType.Linked;
identityField.linkedId = IdentityLinkedId.LicenseNumber;
expect(identityField.toSdkField().linkedId).toBe(415);
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -73,4 +75,19 @@ export class Field extends Domain {
value,
});
}
/**
* Maps Field to SDK format.
*
* @returns {SdkField} The SDK field object.
*/
toSdkField(): SdkField {
return {
name: this.name?.toJSON(),
value: this.value?.toJSON(),
type: this.type,
// Safe type cast: client and SDK LinkedIdType enums have identical values
linkedId: this.linkedId as unknown as SdkLinkedIdType,
};
}
}

View File

@@ -184,4 +184,32 @@ describe("Identity", () => {
expect(Identity.fromJSON(null)).toBeNull();
});
});
describe("toSdkIdentity", () => {
it("returns the correct SDK Identity object", () => {
const identity = new Identity(data);
const sdkIdentity = identity.toSdkIdentity();
expect(sdkIdentity).toEqual({
title: "enctitle",
firstName: "encfirstName",
middleName: "encmiddleName",
lastName: "enclastName",
address1: "encaddress1",
address2: "encaddress2",
address3: "encaddress3",
city: "enccity",
state: "encstate",
postalCode: "encpostalCode",
country: "enccountry",
company: "enccompany",
email: "encemail",
phone: "encphone",
ssn: "encssn",
username: "encusername",
passportNumber: "encpassportNumber",
licenseNumber: "enclicenseNumber",
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Identity as SdkIdentity } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -165,4 +167,32 @@ export class Identity extends Domain {
licenseNumber,
});
}
/**
* Maps Identity to SDK format.
*
* @returns {SdkIdentity} The SDK identity object.
*/
toSdkIdentity(): SdkIdentity {
return {
title: this.title?.toJSON(),
firstName: this.firstName?.toJSON(),
middleName: this.middleName?.toJSON(),
lastName: this.lastName?.toJSON(),
address1: this.address1?.toJSON(),
address2: this.address2?.toJSON(),
address3: this.address3?.toJSON(),
city: this.city?.toJSON(),
state: this.state?.toJSON(),
postalCode: this.postalCode?.toJSON(),
country: this.country?.toJSON(),
company: this.company?.toJSON(),
email: this.email?.toJSON(),
phone: this.phone?.toJSON(),
ssn: this.ssn?.toJSON(),
username: this.username?.toJSON(),
passportNumber: this.passportNumber?.toJSON(),
licenseNumber: this.licenseNumber?.toJSON(),
};
}
}

View File

@@ -1,6 +1,8 @@
import { MockProxy, mock } from "jest-mock-extended";
import { Jsonify } from "type-fest";
import { UriMatchType } from "@bitwarden/sdk-internal";
import { mockEnc, mockFromJson } from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
@@ -118,4 +120,17 @@ describe("LoginUri", () => {
expect(LoginUri.fromJSON(null)).toBeNull();
});
});
describe("SDK Login Uri Mapping", () => {
it("should map to SDK login uri", () => {
const loginUri = new LoginUri(data);
const sdkLoginUri = loginUri.toSdkLoginUri();
expect(sdkLoginUri).toEqual({
uri: "encUri",
uriChecksum: "encUriChecksum",
match: UriMatchType.Domain,
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base";
@@ -87,4 +89,17 @@ export class LoginUri extends Domain {
uriChecksum,
});
}
/**
* Maps LoginUri to SDK format.
*
* @returns {SdkLoginUri} The SDK login uri object.
*/
toSdkLoginUri(): SdkLoginUri {
return {
uri: this.uri.toJSON(),
uriChecksum: this.uriChecksum.toJSON(),
match: this.match,
};
}
}

View File

@@ -202,6 +202,54 @@ describe("Login DTO", () => {
expect(Login.fromJSON(null)).toBeNull();
});
});
describe("toSdkLogin", () => {
it("should map to SDK login", () => {
const data: LoginData = {
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "123",
autofillOnPageLoad: false,
fido2Credentials: [initializeFido2Credential(new Fido2CredentialData())],
};
const login = new Login(data);
const sdkLogin = login.toSdkLogin();
expect(sdkLogin).toEqual({
username: "username",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
uris: [
{
match: 0,
uri: "uri",
uriChecksum: "checksum",
},
],
totp: "123",
autofillOnPageLoad: false,
fido2Credentials: [
{
credentialId: "credentialId",
keyType: "public-key",
keyAlgorithm: "ECDSA",
keyCurve: "P-256",
keyValue: "keyValue",
rpId: "rpId",
userHandle: "userHandle",
userName: "userName",
counter: "counter",
rpName: "rpName",
userDisplayName: "userDisplayName",
discoverable: "discoverable",
creationDate: "2023-01-01T12:00:00.000Z",
},
],
});
});
});
});
type Fido2CredentialLike = Fido2CredentialData | Fido2CredentialView | Fido2CredentialApi;

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Login as SdkLogin } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -144,4 +146,21 @@ export class Login extends Domain {
fido2Credentials,
});
}
/**
* Maps Login to SDK format.
*
* @returns {SdkLogin} The SDK login object.
*/
toSdkLogin(): SdkLogin {
return {
uris: this.uris?.map((u) => u.toSdkLoginUri()),
username: this.username?.toJSON(),
password: this.password?.toJSON(),
passwordRevisionDate: this.passwordRevisionDate?.toISOString(),
totp: this.totp?.toJSON(),
autofillOnPageLoad: this.autofillOnPageLoad,
fido2Credentials: this.fido2Credentials?.map((f) => f.toSdkFido2Credential()),
};
}
}

View File

@@ -70,4 +70,17 @@ describe("Password", () => {
expect(Password.fromJSON(null)).toBeNull();
});
});
describe("toSdkPasswordHistory", () => {
it("returns the correct SDK PasswordHistory object", () => {
const password = new Password(data);
const sdkPasswordHistory = password.toSdkPasswordHistory();
expect(sdkPasswordHistory).toEqual({
password: "encPassword",
lastUsedDate: new Date("2022-01-31T12:00:00.000Z").toISOString(),
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { PasswordHistory } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -57,4 +59,16 @@ export class Password extends Domain {
lastUsedDate,
});
}
/**
* Maps Password to SDK format.
*
* @returns {PasswordHistory} The SDK password history object.
*/
toSdkPasswordHistory(): PasswordHistory {
return {
password: this.password.toJSON(),
lastUsedDate: this.lastUsedDate.toISOString(),
};
}
}

View File

@@ -50,4 +50,17 @@ describe("SecureNote", () => {
expect(SecureNote.fromJSON(null)).toBeNull();
});
});
describe("toSdkSecureNote", () => {
it("returns the correct SDK SecureNote object", () => {
const secureNote = new SecureNote();
secureNote.type = SecureNoteType.Generic;
const sdkSecureNote = secureNote.toSdkSecureNote();
expect(sdkSecureNote).toEqual({
type: 0,
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { SecureNoteType } from "../../enums";
@@ -41,4 +43,15 @@ export class SecureNote extends Domain {
return Object.assign(new SecureNote(), obj);
}
/**
* Maps Secure note to SDK format.
*
* @returns {SdkSecureNote} The SDK secure note object.
*/
toSdkSecureNote(): SdkSecureNote {
return {
type: this.type,
};
}
}

View File

@@ -64,4 +64,17 @@ describe("Sshkey", () => {
expect(SshKey.fromJSON(null)).toBeNull();
});
});
describe("toSdkSshKey", () => {
it("returns the correct SDK SshKey object", () => {
const sshKey = new SshKey(data);
const sdkSshKey = sshKey.toSdkSshKey();
expect(sdkSshKey).toEqual({
privateKey: "privateKey",
publicKey: "publicKey",
fingerprint: "keyFingerprint",
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal";
import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -70,4 +72,17 @@ export class SshKey extends Domain {
keyFingerprint,
});
}
/**
* Maps SSH key to SDK format.
*
* @returns {SdkSshKey} The SDK SSH key object.
*/
toSdkSshKey(): SdkSshKey {
return {
privateKey: this.privateKey.toJSON(),
publicKey: this.publicKey.toJSON(),
fingerprint: this.keyFingerprint.toJSON(),
};
}
}

View File

@@ -1,4 +1,7 @@
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
import { mockFromJson } from "../../../../spec";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { AttachmentView } from "./attachment.view";
@@ -15,4 +18,56 @@ describe("AttachmentView", () => {
expect(actual.key).toEqual("encKeyB64_fromJSON");
});
describe("fromSdkAttachmentView", () => {
it("should return undefined when the input is null", () => {
const result = AttachmentView.fromSdkAttachmentView(null as unknown as any);
expect(result).toBeUndefined();
});
it("should return an AttachmentView from an SdkAttachmentView", () => {
const sdkAttachmentView = {
id: "id",
url: "url",
size: "size",
sizeName: "sizeName",
fileName: "fileName",
key: "encKeyB64_fromString",
} as SdkAttachmentView;
const result = AttachmentView.fromSdkAttachmentView(sdkAttachmentView);
expect(result).toMatchObject({
id: "id",
url: "url",
size: "size",
sizeName: "sizeName",
fileName: "fileName",
key: null,
encryptedKey: new EncString(sdkAttachmentView.key as string),
});
});
});
describe("toSdkAttachmentView", () => {
it("should convert AttachmentView to SdkAttachmentView", () => {
const attachmentView = new AttachmentView();
attachmentView.id = "id";
attachmentView.url = "url";
attachmentView.size = "size";
attachmentView.sizeName = "sizeName";
attachmentView.fileName = "fileName";
attachmentView.encryptedKey = new EncString("encKeyB64");
const result = attachmentView.toSdkAttachmentView();
expect(result).toEqual({
id: "id",
url: "url",
size: "size",
sizeName: "sizeName",
fileName: "fileName",
key: "encKeyB64",
});
});
});
});

View File

@@ -2,7 +2,10 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { Attachment } from "../domain/attachment";
@@ -13,6 +16,10 @@ export class AttachmentView implements View {
sizeName: string = null;
fileName: string = null;
key: SymmetricCryptoKey = null;
/**
* The SDK returns an encrypted key for the attachment.
*/
encryptedKey: EncString | undefined;
constructor(a?: Attachment) {
if (!a) {
@@ -40,4 +47,37 @@ export class AttachmentView implements View {
const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key);
return Object.assign(new AttachmentView(), obj, { key: key });
}
/**
* Converts the AttachmentView to a SDK AttachmentView.
*/
toSdkAttachmentView(): SdkAttachmentView {
return {
id: this.id,
url: this.url,
size: this.size,
sizeName: this.sizeName,
fileName: this.fileName,
key: this.encryptedKey?.toJSON(),
};
}
/**
* Converts the SDK AttachmentView to a AttachmentView.
*/
static fromSdkAttachmentView(obj: SdkAttachmentView): AttachmentView | undefined {
if (!obj) {
return undefined;
}
const view = new AttachmentView();
view.id = obj.id ?? null;
view.url = obj.url ?? null;
view.size = obj.size ?? null;
view.sizeName = obj.sizeName ?? null;
view.fileName = obj.fileName ?? null;
view.encryptedKey = new EncString(obj.key);
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { CardView as SdkCardView } from "@bitwarden/sdk-internal";
import { normalizeExpiryYearFormat } from "../../../autofill/utils";
import { CardLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
@@ -146,4 +148,15 @@ export class CardView extends ItemView {
return null;
}
/**
* Converts an SDK CardView to a CardView.
*/
static fromSdkCardView(obj: SdkCardView): CardView | undefined {
if (obj == null) {
return undefined;
}
return Object.assign(new CardView(), obj);
}
}

View File

@@ -1,4 +1,16 @@
import { mockFromJson } from "../../../../spec";
import {
CipherView as SdkCipherView,
CipherType as SdkCipherType,
CipherRepromptType as SdkCipherRepromptType,
AttachmentView as SdkAttachmentView,
LoginUriView as SdkLoginUriView,
LoginView as SdkLoginView,
FieldView as SdkFieldView,
FieldType as SdkFieldType,
} from "@bitwarden/sdk-internal";
import { mockFromJson, mockFromSdk } from "../../../../spec";
import { CipherRepromptType } from "../../enums";
import { CipherType } from "../../enums/cipher-type";
import { AttachmentView } from "./attachment.view";
@@ -9,6 +21,7 @@ import { IdentityView } from "./identity.view";
import { LoginView } from "./login.view";
import { PasswordHistoryView } from "./password-history.view";
import { SecureNoteView } from "./secure-note.view";
import { SshKeyView } from "./ssh-key.view";
jest.mock("../../models/view/login.view");
jest.mock("../../models/view/attachment.view");
@@ -73,4 +86,121 @@ describe("CipherView", () => {
expect(actual).toMatchObject(expected);
});
});
describe("fromSdkCipherView", () => {
let sdkCipherView: SdkCipherView;
beforeEach(() => {
jest.spyOn(CardView, "fromSdkCardView").mockImplementation(mockFromSdk);
jest.spyOn(IdentityView, "fromSdkIdentityView").mockImplementation(mockFromSdk);
jest.spyOn(LoginView, "fromSdkLoginView").mockImplementation(mockFromSdk);
jest.spyOn(SecureNoteView, "fromSdkSecureNoteView").mockImplementation(mockFromSdk);
jest.spyOn(SshKeyView, "fromSdkSshKeyView").mockImplementation(mockFromSdk);
jest.spyOn(AttachmentView, "fromSdkAttachmentView").mockImplementation(mockFromSdk);
jest.spyOn(FieldView, "fromSdkFieldView").mockImplementation(mockFromSdk);
sdkCipherView = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
collectionIds: ["collectionId"],
key: undefined,
name: "name",
notes: undefined,
type: SdkCipherType.Login,
favorite: true,
edit: true,
reprompt: SdkCipherRepromptType.None,
organizationUseTotp: false,
viewPassword: true,
localData: undefined,
permissions: undefined,
attachments: [{ id: "attachmentId", url: "attachmentUrl" } as SdkAttachmentView],
login: {
username: "username",
password: "password",
uris: [{ uri: "bitwarden.com" } as SdkLoginUriView],
totp: "totp",
autofillOnPageLoad: true,
} as SdkLoginView,
identity: undefined,
card: undefined,
secureNote: undefined,
sshKey: undefined,
fields: [
{
name: "fieldName",
value: "fieldValue",
type: SdkFieldType.Linked,
linkedId: 100,
} as SdkFieldView,
],
passwordHistory: undefined,
creationDate: "2022-01-01T12:00:00.000Z",
revisionDate: "2022-01-02T12:00:00.000Z",
deletedDate: undefined,
};
});
it("returns undefined when input is null", () => {
expect(CipherView.fromSdkCipherView(null as unknown as SdkCipherView)).toBeUndefined();
});
it("maps properties correctly", () => {
const result = CipherView.fromSdkCipherView(sdkCipherView);
expect(result).toMatchObject({
id: "id",
organizationId: "orgId",
folderId: "folderId",
collectionIds: ["collectionId"],
name: "name",
notes: null,
type: CipherType.Login,
favorite: true,
edit: true,
reprompt: CipherRepromptType.None,
organizationUseTotp: false,
viewPassword: true,
localData: undefined,
permissions: undefined,
attachments: [
{
id: "attachmentId",
url: "attachmentUrl",
__fromSdk: true,
},
],
login: {
username: "username",
password: "password",
uris: [
{
uri: "bitwarden.com",
},
],
totp: "totp",
autofillOnPageLoad: true,
__fromSdk: true,
},
identity: new IdentityView(),
card: new CardView(),
secureNote: new SecureNoteView(),
sshKey: new SshKeyView(),
fields: [
{
name: "fieldName",
value: "fieldValue",
type: SdkFieldType.Linked,
linkedId: 100,
__fromSdk: true,
},
],
passwordHistory: null,
creationDate: new Date("2022-01-01T12:00:00.000Z"),
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
deletedDate: null,
});
});
});
});

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
@@ -110,7 +112,7 @@ export class CipherView implements View, InitializerMetadata {
get hasOldAttachments(): boolean {
if (this.hasAttachments) {
for (let i = 0; i < this.attachments.length; i++) {
if (this.attachments[i].key == null) {
if (this.attachments[i].key == null && this.attachments[i].encryptedKey == null) {
return true;
}
}
@@ -222,4 +224,68 @@ export class CipherView implements View, InitializerMetadata {
return view;
}
/**
* Creates a CipherView from the SDK CipherView.
*/
static fromSdkCipherView(obj: SdkCipherView): CipherView | undefined {
if (obj == null) {
return undefined;
}
const cipherView = new CipherView();
cipherView.id = obj.id ?? null;
cipherView.organizationId = obj.organizationId ?? null;
cipherView.folderId = obj.folderId ?? null;
cipherView.name = obj.name;
cipherView.notes = obj.notes ?? null;
cipherView.type = obj.type;
cipherView.favorite = obj.favorite;
cipherView.organizationUseTotp = obj.organizationUseTotp;
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
cipherView.edit = obj.edit;
cipherView.viewPassword = obj.viewPassword;
cipherView.localData = obj.localData
? {
lastUsedDate: obj.localData.lastUsedDate
? new Date(obj.localData.lastUsedDate).getTime()
: undefined,
lastLaunched: obj.localData.lastLaunched
? new Date(obj.localData.lastLaunched).getTime()
: undefined,
}
: undefined;
cipherView.attachments =
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? null;
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? null;
cipherView.passwordHistory =
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? null;
cipherView.collectionIds = obj.collectionIds ?? null;
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
switch (obj.type) {
case CipherType.Card:
cipherView.card = CardView.fromSdkCardView(obj.card);
break;
case CipherType.Identity:
cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity);
break;
case CipherType.Login:
cipherView.login = LoginView.fromSdkLoginView(obj.login);
break;
case CipherType.SecureNote:
cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote);
break;
case CipherType.SshKey:
cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey);
break;
default:
break;
}
return cipherView;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { Fido2CredentialView as SdkFido2CredentialView } from "@bitwarden/sdk-internal";
import { ItemView } from "./item.view";
export class Fido2CredentialView extends ItemView {
@@ -29,4 +31,29 @@ export class Fido2CredentialView extends ItemView {
creationDate,
});
}
/**
* Converts the SDK Fido2CredentialView to a Fido2CredentialView.
*/
static fromSdkFido2CredentialView(obj: SdkFido2CredentialView): Fido2CredentialView | undefined {
if (!obj) {
return undefined;
}
const view = new Fido2CredentialView();
view.credentialId = obj.credentialId;
view.keyType = obj.keyType as "public-key";
view.keyAlgorithm = obj.keyAlgorithm as "ECDSA";
view.keyCurve = obj.keyCurve as "P-256";
view.rpId = obj.rpId;
view.userHandle = obj.userHandle;
view.userName = obj.userName;
view.counter = parseInt(obj.counter);
view.rpName = obj.rpName;
view.userDisplayName = obj.userDisplayName;
view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false;
view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null;
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { FieldView as SdkFieldView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { FieldType, LinkedIdType } from "../../enums";
import { Field } from "../domain/field";
@@ -31,4 +33,21 @@ export class FieldView implements View {
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
return Object.assign(new FieldView(), obj);
}
/**
* Converts the SDK FieldView to a FieldView.
*/
static fromSdkFieldView(obj: SdkFieldView): FieldView | undefined {
if (!obj) {
return undefined;
}
const view = new FieldView();
view.name = obj.name;
view.value = obj.value;
view.type = obj.type;
view.linkedId = obj.linkedId as unknown as LinkedIdType;
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal";
import { Utils } from "../../../platform/misc/utils";
import { IdentityLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator";
@@ -158,4 +160,15 @@ export class IdentityView extends ItemView {
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
return Object.assign(new IdentityView(), obj);
}
/**
* Converts the SDK IdentityView to an IdentityView.
*/
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined {
if (obj == null) {
return undefined;
}
return Object.assign(new IdentityView(), obj);
}
}

View File

@@ -1,3 +1,5 @@
import { LoginUriView as SdkLoginUriView, UriMatchType } from "@bitwarden/sdk-internal";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
@@ -184,6 +186,26 @@ describe("LoginUriView", () => {
});
});
});
describe("fromSdkLoginUriView", () => {
it("should return undefined when the input is null", () => {
const result = LoginUriView.fromSdkLoginUriView(null as unknown as SdkLoginUriView);
expect(result).toBeUndefined();
});
it("should create a LoginUriView from a SdkLoginUriView", () => {
const sdkLoginUriView = {
uri: "https://example.com",
match: UriMatchType.Host,
} as SdkLoginUriView;
const loginUriView = LoginUriView.fromSdkLoginUriView(sdkLoginUriView);
expect(loginUriView).toBeInstanceOf(LoginUriView);
expect(loginUriView!.uri).toBe(sdkLoginUriView.uri);
expect(loginUriView!.match).toBe(sdkLoginUriView.match);
});
});
});
function uriFactory(match: UriMatchStrategySetting, uri: string) {

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { View } from "../../../models/view/view";
import { SafeUrls } from "../../../platform/misc/safe-urls";
@@ -112,6 +114,21 @@ export class LoginUriView implements View {
return Object.assign(new LoginUriView(), obj);
}
/**
* Converts a LoginUriView object from the SDK to a LoginUriView object.
*/
static fromSdkLoginUriView(obj: SdkLoginUriView): LoginUriView | undefined {
if (obj == null) {
return undefined;
}
const view = new LoginUriView();
view.uri = obj.uri;
view.match = obj.match;
return view;
}
matchesUri(
targetUri: string,
equivalentDomains: Set<string>,

View File

@@ -1,4 +1,6 @@
import { mockFromJson } from "../../../../spec";
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
import { mockFromJson, mockFromSdk } from "../../../../spec";
import { LoginUriView } from "./login-uri.view";
import { LoginView } from "./login.view";
@@ -25,4 +27,35 @@ describe("LoginView", () => {
uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"],
});
});
describe("fromSdkLoginView", () => {
it("should return undefined when the input is null", () => {
const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView);
expect(result).toBeUndefined();
});
it("should return a LoginView from an SdkLoginView", () => {
jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk);
const sdkLoginView = {
username: "username",
password: "password",
passwordRevisionDate: "2025-01-01T01:06:40.441Z",
uris: [{ uri: "bitwarden.com" } as any],
totp: "totp",
autofillOnPageLoad: true,
} as SdkLoginView;
const result = LoginView.fromSdkLoginView(sdkLoginView);
expect(result).toMatchObject({
username: "username",
password: "password",
passwordRevisionDate: new Date("2025-01-01T01:06:40.441Z"),
uris: [expect.objectContaining({ uri: "bitwarden.com", __fromSdk: true })],
totp: "totp",
autofillOnPageLoad: true,
});
});
});
});

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils";
import { DeepJsonify } from "../../../types/deep-jsonify";
@@ -100,4 +102,27 @@ export class LoginView extends ItemView {
fido2Credentials,
});
}
/**
* Converts the SDK LoginView to a LoginView.
*
* Note: FIDO2 credentials remain encrypted at this stage.
* Unlike other fields that are decrypted as part of the LoginView, the SDK maintains
* the FIDO2 credentials in encrypted form. We can decrypt them later using a separate
* call to client.vault().ciphers().decrypt_fido2_credentials().
*/
static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined {
if (obj == null) {
return undefined;
}
const passwordRevisionDate =
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri));
return Object.assign(new LoginView(), obj, {
passwordRevisionDate,
uris,
});
}
}

View File

@@ -1,3 +1,5 @@
import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal";
import { PasswordHistoryView } from "./password-history.view";
describe("PasswordHistoryView", () => {
@@ -10,4 +12,25 @@ describe("PasswordHistoryView", () => {
expect(actual.lastUsedDate).toEqual(lastUsedDate);
});
describe("fromSdkPasswordHistoryView", () => {
it("should return undefined when the input is null", () => {
const result = PasswordHistoryView.fromSdkPasswordHistoryView(null as unknown as any);
expect(result).toBeUndefined();
});
it("should return a PasswordHistoryView from an SdkPasswordHistoryView", () => {
const sdkPasswordHistoryView = {
password: "password",
lastUsedDate: "2023-10-01T00:00:00Z",
} as SdkPasswordHistoryView;
const result = PasswordHistoryView.fromSdkPasswordHistoryView(sdkPasswordHistoryView);
expect(result).toMatchObject({
password: "password",
lastUsedDate: new Date("2023-10-01T00:00:00Z"),
});
});
});
});

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { PasswordHistoryView as SdkPasswordHistoryView } from "@bitwarden/sdk-internal";
import { View } from "../../../models/view/view";
import { Password } from "../domain/password";
@@ -24,4 +26,19 @@ export class PasswordHistoryView implements View {
lastUsedDate: lastUsedDate,
});
}
/**
* Converts the SDK PasswordHistoryView to a PasswordHistoryView.
*/
static fromSdkPasswordHistoryView(obj: SdkPasswordHistoryView): PasswordHistoryView | undefined {
if (!obj) {
return undefined;
}
const view = new PasswordHistoryView();
view.password = obj.password;
view.lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate);
return view;
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal";
import { SecureNoteType } from "../../enums";
import { SecureNote } from "../domain/secure-note";
@@ -26,4 +28,15 @@ export class SecureNoteView extends ItemView {
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
return Object.assign(new SecureNoteView(), obj);
}
/**
* Converts the SDK SecureNoteView to a SecureNoteView.
*/
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined {
if (!obj) {
return undefined;
}
return Object.assign(new SecureNoteView(), obj);
}
}

View File

@@ -2,6 +2,8 @@
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal";
import { SshKey } from "../domain/ssh-key";
import { ItemView } from "./item.view";
@@ -44,4 +46,19 @@ export class SshKeyView extends ItemView {
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
return Object.assign(new SshKeyView(), obj);
}
/**
* Converts the SDK SshKeyView to a SshKeyView.
*/
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined {
if (!obj) {
return undefined;
}
const keyFingerprint = obj.fingerprint;
return Object.assign(new SshKeyView(), obj, {
keyFingerprint,
});
}
}

View File

@@ -6,7 +6,7 @@ import { CipherDecryptionKeys, KeyService } from "@bitwarden/key-management";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
import { makeStaticByteArray } from "../../../spec/utils";
import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service";
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
@@ -24,6 +24,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
import { ContainerService } from "../../platform/services/container.service";
import { CipherId, UserId } from "../../types/guid";
import { CipherKey, OrgKey, UserKey } from "../../types/key";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums";
import { CipherRepromptType } from "../enums/cipher-reprompt-type";
@@ -34,6 +35,7 @@ import { Cipher } from "../models/domain/cipher";
import { CipherCreateRequest } from "../models/request/cipher-create.request";
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
import { CipherRequest } from "../models/request/cipher.request";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { LoginUriView } from "../models/view/login-uri.view";
@@ -124,6 +126,7 @@ describe("Cipher Service", () => {
accountService = mockAccountServiceWith(mockUserId);
const logService = mock<LogService>();
const stateProvider = new FakeStateProvider(accountService);
const cipherEncryptionService = mock<CipherEncryptionService>();
const userId = "TestUserId" as UserId;
@@ -151,6 +154,7 @@ describe("Cipher Service", () => {
stateProvider,
accountService,
logService,
cipherEncryptionService,
);
cipherObj = new Cipher(cipherData);
@@ -478,4 +482,85 @@ describe("Cipher Service", () => {
).rejects.toThrow("Cannot rotate ciphers when decryption failures are present");
});
});
describe("decrypt", () => {
it("should call decrypt method of CipherEncryptionService when feature flag is true", async () => {
configService.getFeatureFlag.mockResolvedValue(true);
cipherEncryptionService.decrypt.mockResolvedValue(new CipherView(cipherObj));
const result = await cipherService.decrypt(cipherObj, userId);
expect(result).toEqual(new CipherView(cipherObj));
expect(cipherEncryptionService.decrypt).toHaveBeenCalledWith(cipherObj, userId);
});
it("should call legacy decrypt when feature flag is false", async () => {
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey;
configService.getFeatureFlag.mockResolvedValue(false);
cipherService.getKeyForCipherKeyDecryption = jest.fn().mockResolvedValue(mockUserKey);
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
jest.spyOn(cipherObj, "decrypt").mockResolvedValue(new CipherView(cipherObj));
const result = await cipherService.decrypt(cipherObj, userId);
expect(result).toEqual(new CipherView(cipherObj));
expect(cipherObj.decrypt).toHaveBeenCalledWith(mockUserKey);
});
});
describe("getDecryptedAttachmentBuffer", () => {
const mockEncryptedContent = new Uint8Array([1, 2, 3]);
const mockDecryptedContent = new Uint8Array([4, 5, 6]);
it("should use SDK when feature flag is enabled", async () => {
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
configService.getFeatureFlag.mockResolvedValue(true);
jest.spyOn(cipherService, "ciphers$").mockReturnValue(of({ [cipher.id]: cipherData }));
cipherEncryptionService.decryptAttachmentContent.mockResolvedValue(mockDecryptedContent);
const mockResponse = {
arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer),
} as unknown as Response;
const result = await cipherService.getDecryptedAttachmentBuffer(
cipher.id as CipherId,
attachment,
mockResponse,
userId,
);
expect(result).toEqual(mockDecryptedContent);
expect(cipherEncryptionService.decryptAttachmentContent).toHaveBeenCalledWith(
cipher,
attachment,
mockEncryptedContent,
userId,
);
});
it("should use legacy decryption when feature flag is enabled", async () => {
configService.getFeatureFlag.mockResolvedValue(false);
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
attachment.key = makeSymmetricCryptoKey(64);
const mockResponse = {
arrayBuffer: jest.fn().mockResolvedValue(mockEncryptedContent.buffer),
} as unknown as Response;
const mockEncBuf = {} as EncArrayBuffer;
EncArrayBuffer.fromResponse = jest.fn().mockResolvedValue(mockEncBuf);
encryptService.decryptFileData.mockResolvedValue(mockDecryptedContent);
const result = await cipherService.getDecryptedAttachmentBuffer(
cipher.id as CipherId,
attachment,
mockResponse,
userId,
);
expect(result).toEqual(mockDecryptedContent);
expect(encryptService.decryptFileData).toHaveBeenCalledWith(mockEncBuf, attachment.key);
});
});
});

View File

@@ -29,7 +29,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
import { StateProvider } from "../../platform/state";
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
import { OrgKey, UserKey } from "../../types/key";
import { perUserCache$ } from "../../vault/utils/observable-utilities";
import { filterOutNullish, perUserCache$ } from "../../vault/utils/observable-utilities";
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums";
@@ -103,6 +104,7 @@ export class CipherService implements CipherServiceAbstraction {
private stateProvider: StateProvider,
private accountService: AccountService,
private logService: LogService,
private cipherEncryptionService: CipherEncryptionService,
) {}
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
@@ -424,13 +426,21 @@ export class CipherService implements CipherServiceAbstraction {
ciphers: Cipher[],
userId: UserId,
): Promise<[CipherView[], CipherView[]] | null> {
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
const decryptStartTime = new Date().getTime();
const decrypted = await this.decryptCiphersWithSdk(ciphers, userId);
this.logService.info(
`[CipherService] Decrypting ${decrypted.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
);
// With SDK, failed ciphers are not returned
return [decrypted, []];
}
const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true));
if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) {
// return early if there are no keys to decrypt with
return null;
}
// Group ciphers by orgId or under 'null' for the user's ciphers
const grouped = ciphers.reduce(
(agg, c) => {
@@ -440,7 +450,6 @@ export class CipherService implements CipherServiceAbstraction {
},
{} as Record<string, Cipher[]>,
);
const decryptStartTime = new Date().getTime();
const allCipherViews = (
await Promise.all(
@@ -464,7 +473,6 @@ export class CipherService implements CipherServiceAbstraction {
this.logService.info(
`[CipherService] Decrypting ${allCipherViews.length} ciphers took ${new Date().getTime() - decryptStartTime}ms`,
);
// Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt
return allCipherViews.reduce(
(acc, c) => {
@@ -479,6 +487,21 @@ export class CipherService implements CipherServiceAbstraction {
);
}
/**
* Decrypts a cipher using either the SDK or the legacy method based on the feature flag.
* @param cipher The cipher to decrypt.
* @param userId The user ID to use for decryption.
* @returns A promise that resolves to the decrypted cipher view.
*/
async decrypt(cipher: Cipher, userId: UserId): Promise<CipherView> {
if (await this.configService.getFeatureFlag(FeatureFlag.PM19941MigrateCipherDomainToSdk)) {
return await this.cipherEncryptionService.decrypt(cipher, userId);
} else {
const encKey = await this.getKeyForCipherKeyDecryption(cipher, userId);
return await cipher.decrypt(encKey);
}
}
private async reindexCiphers(userId: UserId) {
const reindexRequired =
this.searchService != null &&
@@ -895,7 +918,7 @@ export class CipherService implements CipherServiceAbstraction {
//then we rollback to using the user key as the main key of encryption of the item
//in order to keep item and it's attachments with the same encryption level
if (cipher.key != null && !cipherKeyEncryptionEnabled) {
const model = await cipher.decrypt(await this.getKeyForCipherKeyDecryption(cipher, userId));
const model = await this.decrypt(cipher, userId);
cipher = await this.encrypt(model, userId);
await this.updateWithServer(cipher);
}
@@ -1381,6 +1404,43 @@ export class CipherService implements CipherServiceAbstraction {
return encryptedCiphers;
}
async getDecryptedAttachmentBuffer(
cipherId: CipherId,
attachment: AttachmentView,
response: Response,
userId: UserId,
): Promise<Uint8Array> {
const useSdkDecryption = await this.configService.getFeatureFlag(
FeatureFlag.PM19941MigrateCipherDomainToSdk,
);
const cipherDomain = await firstValueFrom(
this.ciphers$(userId).pipe(map((ciphersData) => new Cipher(ciphersData[cipherId]))),
);
if (useSdkDecryption) {
const encryptedContent = await response.arrayBuffer();
return this.cipherEncryptionService.decryptAttachmentContent(
cipherDomain,
attachment,
new Uint8Array(encryptedContent),
userId,
);
}
const encBuf = await EncArrayBuffer.fromResponse(response);
const key =
attachment.key != null
? attachment.key
: await firstValueFrom(
this.keyService.orgKeys$(userId).pipe(
filterOutNullish(),
map((orgKeys) => orgKeys[cipherDomain.organizationId as OrganizationId] as OrgKey),
),
);
return await this.encryptService.decryptFileData(encBuf, key);
}
/**
* @returns a SingleUserState
*/
@@ -1430,9 +1490,7 @@ export class CipherService implements CipherServiceAbstraction {
originalCipher: Cipher,
userId: UserId,
): Promise<void> {
const existingCipher = await originalCipher.decrypt(
await this.getKeyForCipherKeyDecryption(originalCipher, userId),
);
const existingCipher = await this.decrypt(originalCipher, userId);
model.passwordHistory = existingCipher.passwordHistory || [];
if (model.type === CipherType.Login && existingCipher.type === CipherType.Login) {
if (
@@ -1852,4 +1910,17 @@ export class CipherService implements CipherServiceAbstraction {
);
return featureEnabled && meetsServerVersion;
}
/**
* Decrypts the provided ciphers using the SDK.
* @param ciphers The ciphers to decrypt.
* @param userId The user ID to use for decryption.
* @returns The decrypted ciphers.
* @private
*/
private async decryptCiphersWithSdk(ciphers: Cipher[], userId: UserId): Promise<CipherView[]> {
const decryptedViews = await this.cipherEncryptionService.decryptManyLegacy(ciphers, userId);
return decryptedViews.sort(this.getLocaleSortingFunction());
}
}

View File

@@ -0,0 +1,334 @@
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import {
Fido2Credential,
Cipher as SdkCipher,
CipherType as SdkCipherType,
CipherView as SdkCipherView,
CipherListView,
Attachment as SdkAttachment,
} from "@bitwarden/sdk-internal";
import { mockEnc } from "../../../spec";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { LogService } from "../../platform/abstractions/log.service";
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
import { UserId } from "../../types/guid";
import { CipherRepromptType, CipherType } from "../enums";
import { CipherPermissionsApi } from "../models/api/cipher-permissions.api";
import { CipherData } from "../models/data/cipher.data";
import { Cipher } from "../models/domain/cipher";
import { AttachmentView } from "../models/view/attachment.view";
import { CipherView } from "../models/view/cipher.view";
import { Fido2CredentialView } from "../models/view/fido2-credential.view";
import { DefaultCipherEncryptionService } from "./default-cipher-encryption.service";
const cipherData: CipherData = {
id: "id",
organizationId: "orgId",
folderId: "folderId",
edit: true,
viewPassword: true,
organizationUseTotp: true,
favorite: false,
revisionDate: "2022-01-31T12:00:00.000Z",
type: CipherType.Login,
name: "EncryptedString",
notes: "EncryptedString",
creationDate: "2022-01-01T12:00:00.000Z",
deletedDate: null,
permissions: new CipherPermissionsApi(),
key: "EncKey",
reprompt: CipherRepromptType.None,
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",
},
],
};
describe("DefaultCipherEncryptionService", () => {
let cipherEncryptionService: DefaultCipherEncryptionService;
const sdkService = mock<SdkService>();
const logService = mock<LogService>();
let sdkCipherView: SdkCipherView;
const mockSdkClient = {
vault: jest.fn().mockReturnValue({
ciphers: jest.fn().mockReturnValue({
decrypt: jest.fn(),
decrypt_list: jest.fn(),
decrypt_fido2_credentials: jest.fn(),
}),
attachments: jest.fn().mockReturnValue({
decrypt_buffer: jest.fn(),
}),
}),
};
const mockRef = {
value: mockSdkClient,
[Symbol.dispose]: jest.fn(),
};
const mockSdk = {
take: jest.fn().mockReturnValue(mockRef),
};
const userId = "user-id" as UserId;
let cipherObj: Cipher;
beforeEach(() => {
sdkService.userClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
cipherEncryptionService = new DefaultCipherEncryptionService(sdkService, logService);
cipherObj = new Cipher(cipherData);
jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => {
return { id: cipherData.id } as SdkCipher;
});
sdkCipherView = {
id: "test-id",
type: SdkCipherType.Login,
name: "test-name",
login: {
username: "test-username",
password: "test-password",
},
} as SdkCipherView;
});
afterEach(() => {
jest.clearAllMocks();
});
describe("decrypt", () => {
it("should decrypt a cipher successfully", async () => {
const expectedCipherView: CipherView = {
id: "test-id",
type: CipherType.Login,
name: "test-name",
login: {
username: "test-username",
password: "test-password",
},
} as CipherView;
mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView);
jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView);
const result = await cipherEncryptionService.decrypt(cipherObj, userId);
expect(result).toEqual(expectedCipherView);
expect(cipherObj.toSdkCipher).toHaveBeenCalledTimes(1);
expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledWith({ id: cipherData.id });
expect(CipherView.fromSdkCipherView).toHaveBeenCalledWith(sdkCipherView);
expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).not.toHaveBeenCalled();
});
it("should decrypt FIDO2 credentials if present", async () => {
const fido2Credentials = [
{
credentialId: mockEnc("credentialId"),
keyType: mockEnc("keyType"),
keyAlgorithm: mockEnc("keyAlgorithm"),
keyCurve: mockEnc("keyCurve"),
keyValue: mockEnc("keyValue"),
rpId: mockEnc("rpId"),
userHandle: mockEnc("userHandle"),
userName: mockEnc("userName"),
counter: mockEnc("2"),
rpName: mockEnc("rpName"),
userDisplayName: mockEnc("userDisplayName"),
discoverable: mockEnc("true"),
creationDate: new Date("2023-01-01T12:00:00.000Z"),
},
] as unknown as Fido2Credential[];
sdkCipherView.login!.fido2Credentials = fido2Credentials;
const expectedCipherView: CipherView = {
id: "test-id",
type: CipherType.Login,
name: "test-name",
login: {
username: "test-username",
password: "test-password",
fido2Credentials: [],
},
} as unknown as CipherView;
const fido2CredentialView: Fido2CredentialView = {
credentialId: "credentialId",
keyType: "keyType",
keyAlgorithm: "keyAlgorithm",
keyCurve: "keyCurve",
keyValue: "decrypted-key-value",
rpId: "rpId",
userHandle: "userHandle",
userName: "userName",
counter: 2,
rpName: "rpName",
userDisplayName: "userDisplayName",
discoverable: true,
creationDate: new Date("2023-01-01T12:00:00.000Z"),
} as unknown as Fido2CredentialView;
mockSdkClient.vault().ciphers().decrypt.mockReturnValue(sdkCipherView);
mockSdkClient.vault().ciphers().decrypt_fido2_credentials.mockReturnValue(fido2Credentials);
mockSdkClient.vault().ciphers().decrypt_fido2_private_key = jest
.fn()
.mockReturnValue("decrypted-key-value");
jest.spyOn(CipherView, "fromSdkCipherView").mockReturnValue(expectedCipherView);
jest
.spyOn(Fido2CredentialView, "fromSdkFido2CredentialView")
.mockReturnValueOnce(fido2CredentialView);
const result = await cipherEncryptionService.decrypt(cipherObj, userId);
expect(result).toBe(expectedCipherView);
expect(result.login?.fido2Credentials).toEqual([fido2CredentialView]);
expect(mockSdkClient.vault().ciphers().decrypt_fido2_credentials).toHaveBeenCalledWith(
sdkCipherView,
);
expect(mockSdkClient.vault().ciphers().decrypt_fido2_private_key).toHaveBeenCalledWith(
sdkCipherView,
);
expect(Fido2CredentialView.fromSdkFido2CredentialView).toHaveBeenCalledTimes(1);
});
});
describe("decryptManyLegacy", () => {
it("should decrypt multiple ciphers successfully", async () => {
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
const expectedViews = [
{
id: "test-id-1",
name: "test-name-1",
} as CipherView,
{
id: "test-id-2",
name: "test-name-2",
} as CipherView,
];
mockSdkClient
.vault()
.ciphers()
.decrypt.mockReturnValueOnce({ id: "test-id-1", name: "test-name-1" } as SdkCipherView)
.mockReturnValueOnce({ id: "test-id-2", name: "test-name-2" } as SdkCipherView);
jest
.spyOn(CipherView, "fromSdkCipherView")
.mockReturnValueOnce(expectedViews[0])
.mockReturnValueOnce(expectedViews[1]);
const result = await cipherEncryptionService.decryptManyLegacy(ciphers, userId);
expect(result).toEqual(expectedViews);
expect(mockSdkClient.vault().ciphers().decrypt).toHaveBeenCalledTimes(2);
expect(CipherView.fromSdkCipherView).toHaveBeenCalledTimes(2);
});
it("should throw EmptyError when SDK is not available", async () => {
sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any;
await expect(
cipherEncryptionService.decryptManyLegacy([cipherObj], userId),
).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to decrypt ciphers"),
);
});
});
describe("decryptMany", () => {
it("should decrypt multiple ciphers to list views", async () => {
const ciphers = [new Cipher(cipherData), new Cipher(cipherData)];
const expectedListViews = [
{ id: "list1", name: "List 1" } as CipherListView,
{ id: "list2", name: "List 2" } as CipherListView,
];
mockSdkClient.vault().ciphers().decrypt_list.mockReturnValue(expectedListViews);
const result = await cipherEncryptionService.decryptMany(ciphers, userId);
expect(result).toEqual(expectedListViews);
expect(mockSdkClient.vault().ciphers().decrypt_list).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({ id: cipherData.id }),
expect.objectContaining({ id: cipherData.id }),
]),
);
});
it("should throw EmptyError when SDK is not available", async () => {
sdkService.userClient$ = jest.fn().mockReturnValue(of(null)) as any;
await expect(cipherEncryptionService.decryptMany([cipherObj], userId)).rejects.toThrow();
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to decrypt cipher list"),
);
});
});
describe("decryptAttachmentContent", () => {
it("should decrypt attachment content successfully", async () => {
const cipher = new Cipher(cipherData);
const attachment = new AttachmentView(cipher.attachments![0]);
const encryptedContent = new Uint8Array([1, 2, 3, 4]);
const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]);
jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher);
jest.spyOn(attachment, "toSdkAttachmentView").mockReturnValue({ id: "a1" } as SdkAttachment);
mockSdkClient.vault().attachments().decrypt_buffer.mockReturnValue(expectedDecryptedContent);
const result = await cipherEncryptionService.decryptAttachmentContent(
cipher,
attachment,
encryptedContent,
userId,
);
expect(result).toEqual(expectedDecryptedContent);
expect(cipher.toSdkCipher).toHaveBeenCalled();
expect(attachment.toSdkAttachmentView).toHaveBeenCalled();
expect(mockSdkClient.vault().attachments().decrypt_buffer).toHaveBeenCalledWith(
{ id: "id" },
{ id: "a1" },
encryptedContent,
);
});
});
});

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