From 9319dbcc90f352dded5ec1f4a2c6b123c56abdaa Mon Sep 17 00:00:00 2001 From: voommen-livefront Date: Fri, 9 May 2025 12:03:30 -0500 Subject: [PATCH] PM-20577 updated encryption/decryption --- .../risk-insights/models/password-health.ts | 9 ++ .../risk-insights-api.service.spec.ts | 6 +- .../services/risk-insights-api.service.ts | 17 ++++ .../services/risk-insights-report.service.ts | 88 +++++++++++++++++-- .../access-intelligence.module.ts | 2 + .../all-applications.component.ts | 87 +++++++++++++++--- .../risk-insights.component.ts | 2 +- 7 files changed, 186 insertions(+), 25 deletions(-) diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts index 050bfb50e76..9fd6f3faa90 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/models/password-health.ts @@ -167,12 +167,21 @@ export interface RiskInsightsReport { totalCriticalApplications: number; } +export interface ReportInsightsReportData { + data: string; + key: string; +} + export interface SaveRiskInsightsReportRequest { data: RiskInsightsReport; } export interface SaveRiskInsightsReportResponse { id: string; +} + +export interface GetRiskInsightsReportResponse { + id: string; organizationId: OrganizationId; date: string; reportData: string; diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts index 1ce297377d2..2e5b6ca5cda 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.spec.ts @@ -3,6 +3,8 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { SaveRiskInsightsReportRequest } from "../models/password-health"; + import { RiskInsightsApiService } from "./risk-insights-api.service"; describe("RiskInsightsApiService", () => { @@ -19,11 +21,11 @@ describe("RiskInsightsApiService", () => { it("should call apiService.send with correct parameters for saveRiskInsightsReport", (done) => { const orgId = "org1" as OrganizationId; - const request = { + const request: SaveRiskInsightsReportRequest = { data: { organizationId: orgId, date: new Date().toISOString(), - reportData: "test data", + reportData: "test", totalMembers: 10, totalAtRiskMembers: 5, totalApplications: 100, diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts index bcf4b1efcd2..786ffbc3722 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-api.service.ts @@ -4,6 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { + GetRiskInsightsReportResponse, SaveRiskInsightsReportRequest, SaveRiskInsightsReportResponse, } from "../models/password-health"; @@ -25,4 +26,20 @@ export class RiskInsightsApiService { return from(dbResponse as Promise); } + + getRiskInsightsReport(orgId: OrganizationId): Observable { + const dbResponse = this.apiService + .send("GET", `/reports/risk-insights-report/${orgId.toString()}`, null, true, true) + .catch((error: any): any => { + if (error.statusCode === 404) { + return null; // Handle 404 by returning null or an appropriate default value + } + throw error; // Re-throw other errors + }); + + if (dbResponse instanceof Error) { + return from(null as Promise); + } + return from(dbResponse as Promise); + } } diff --git a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts index 7c96a9a907e..8dd9ad775d1 100644 --- a/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/dirt/reports/risk-insights/services/risk-insights-report.service.ts @@ -4,7 +4,9 @@ import { concatMap, first, firstValueFrom, from, map, Observable, takeWhile, zip import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -24,6 +26,7 @@ import { WeakPasswordDetail, WeakPasswordScore, RiskInsightsReport, + GetRiskInsightsReportResponse, } from "../models/password-health"; import { CriticalAppsService } from "./critical-apps.service"; @@ -38,6 +41,7 @@ export class RiskInsightsReportService { private keyService: KeyService, private encryptService: EncryptService, private criticalAppsService: CriticalAppsService, + private keyGeneratorService: KeyGenerationService, ) {} /** @@ -170,36 +174,104 @@ export class RiskInsightsReportService { }; } - async generateRiskInsightsReport( + async generateEncryptedRiskInsightsReport( organizationId: OrganizationId, details: ApplicationHealthReportDetail[], summary: ApplicationHealthReportSummary, ): Promise { - const key = await this.keyService.getOrgKey(organizationId as string); - if (key === null) { + const orgKey = await this.keyService.getOrgKey(organizationId as string); + if (orgKey === null) { throw new Error("Organization key not found"); } - const reportData = await this.encryptService.encryptString(JSON.stringify(details), key); + const reportContentEncryptionKey = await this.keyGeneratorService.createKey(512); + const reportEncrypted = await this.encryptService.encryptString( + JSON.stringify(details), + reportContentEncryptionKey, + ); + + const wrappedReportContentEncryptionKey = await this.encryptService.wrapSymmetricKey( + reportContentEncryptionKey, + orgKey, + ); + + const reportDataWithWrappedKey = { + data: reportEncrypted.encryptedString, + key: wrappedReportContentEncryptionKey.encryptedString, + }; + + const encryptedReportDataWithWrappedKey = await this.encryptService.encryptString( + JSON.stringify(reportDataWithWrappedKey), + orgKey, + ); - // const atRiskMembers = this.generateAtRiskMemberList(details); - // const atRiskApplications = this.generateAtRiskApplicationList(details); const criticalApps = await firstValueFrom( this.criticalAppsService .getAppsListForOrg(organizationId) .pipe(takeWhile((apps) => apps !== null && apps.length > 0)), ); - return { + const riskInsightReport = { organizationId: organizationId, date: new Date().toISOString(), - reportData: reportData?.encryptedString?.toString() ?? "", + reportData: encryptedReportDataWithWrappedKey.encryptedString, totalMembers: summary.totalMemberCount, totalAtRiskMembers: summary.totalAtRiskMemberCount, totalApplications: summary.totalApplicationCount, totalAtRiskApplications: summary.totalAtRiskApplicationCount, totalCriticalApplications: criticalApps.length, }; + + return riskInsightReport; + } + + async decryptRiskInsightsReport( + organizationId: OrganizationId, + riskInsightsReportResponse: GetRiskInsightsReportResponse, + ): Promise<[ApplicationHealthReportDetail[], ApplicationHealthReportSummary]> { + try { + const orgKey = await this.keyService.getOrgKey(organizationId as string); + if (orgKey === null) { + throw new Error("Organization key not found"); + } + + const decryptedReportDataWithWrappedKey = await this.encryptService.decryptString( + new EncString(riskInsightsReportResponse.reportData), + orgKey, + ); + + const reportDataInJson = JSON.parse(decryptedReportDataWithWrappedKey); + const reportEncrypted = reportDataInJson.data; + const wrappedReportContentEncryptionKey = reportDataInJson.key; + + const freshWrappedReportContentEncryptionKey = new EncString( + wrappedReportContentEncryptionKey, + ); + + const unwrappedReportContentEncryptionKey = await this.encryptService.unwrapSymmetricKey( + freshWrappedReportContentEncryptionKey, + orgKey, + ); + + const reportUnencrypted = await this.encryptService.decryptString( + new EncString(reportEncrypted), + unwrappedReportContentEncryptionKey, + ); + + const reportJson: ApplicationHealthReportDetail[] = JSON.parse(reportUnencrypted); + + const summary: ApplicationHealthReportSummary = { + totalMemberCount: riskInsightsReportResponse.totalMembers, + totalAtRiskMemberCount: riskInsightsReportResponse.totalAtRiskMembers, + totalApplicationCount: riskInsightsReportResponse.totalApplications, + totalAtRiskApplicationCount: riskInsightsReportResponse.totalAtRiskApplications, + }; + + return [reportJson, summary]; + } catch { + // console.error("Error decrypting risk insights report :", error); + return [null, null]; + } } /** diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts index 5110a05283d..5a4f4cedd9d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/access-intelligence.module.ts @@ -12,6 +12,7 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { KeyService } from "@bitwarden/key-management"; @@ -36,6 +37,7 @@ import { RiskInsightsComponent } from "./risk-insights.component"; KeyService, EncryptService, CriticalAppsService, + KeyGenerationService, ], }, { diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts index b9284cabbb3..465fa7ac128 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/all-applications.component.ts @@ -10,7 +10,6 @@ import { map, Observable, of, - skipWhile, switchMap, } from "rxjs"; @@ -106,23 +105,51 @@ export class AllApplicationsComponent implements OnInit { .pipe(getOrganizationById(organizationId)); combineLatest([ - this.dataService.applications$, + this.riskInsightsApiService.getRiskInsightsReport(organizationId as OrganizationId), this.criticalAppsService.getAppsListForOrg(organizationId), organization$, + this.dataService.applications$, ]) .pipe( - takeUntilDestroyed(this.destroyRef), - skipWhile(([_, __, organization]) => !organization), - map(([applications, criticalApps, organization]) => { + switchMap(async ([reportArchive, criticalApps, organization, appHealthReport]) => { + if (reportArchive == null) { + const report = await firstValueFrom(this.dataService.applications$); + + if (report == null) { + this.dataService.fetchApplicationsReport(organizationId as OrganizationId); + } + + return { + report, + criticalApps, + organization, + fromDb: false, + }; + } else { + const [report] = await this.reportService.decryptRiskInsightsReport( + organizationId as OrganizationId, + reportArchive, + ); + + return { + report, + criticalApps, + organization, + fromDb: true, + }; + } + }), + map(({ report, criticalApps, organization, fromDb }) => { const criticalUrls = criticalApps.map((ca) => ca.uri); - const data = applications?.map((app) => ({ + const data = report?.map((app) => ({ ...app, isMarkedAsCritical: criticalUrls.includes(app.applicationName), })) as ApplicationHealthReportDetailWithCriticalFlag[]; - return { data, organization }; + return { data, organization, fromDb }; }), + takeUntilDestroyed(this.destroyRef), ) - .subscribe(({ data, organization }) => { + .subscribe(({ data, organization, fromDb }) => { if (data) { this.dataSource.data = data; this.applicationSummary = this.reportService.generateApplicationsSummary(data); @@ -131,17 +158,50 @@ export class AllApplicationsComponent implements OnInit { this.organization = organization; } - if (data && organization) { + if (!fromDb && data && organization) { this.atRiskInsightsReport.next({ data, organization, summary: this.applicationSummary, }); } - - // this.persistReportData(); }); + // combineLatest([ + // this.dataService.applications$, + // this.criticalAppsService.getAppsListForOrg(organizationId), + // organization$, + // ]) + // .pipe( + // takeUntilDestroyed(this.destroyRef), + // skipWhile(([_, __, organization]) => !organization), + // map(([applications, criticalApps, organization]) => { + // const criticalUrls = criticalApps.map((ca) => ca.uri); + // const data = applications?.map((app) => ({ + // ...app, + // isMarkedAsCritical: criticalUrls.includes(app.applicationName), + // })) as ApplicationHealthReportDetailWithCriticalFlag[]; + // return { data, organization }; + // }), + // ) + // .subscribe(({ data, organization }) => { + // if (data) { + // this.dataSource.data = data; + // this.applicationSummary = this.reportService.generateApplicationsSummary(data); + // } + // if (organization) { + // this.organization = organization; + // } + + // if (data && organization) { + // this.atRiskInsightsReport.next({ + // data, + // organization, + // summary: this.applicationSummary, + // }); + // } + // }); + this.isLoading$ = this.dataService.isLoading$; } } @@ -169,7 +229,7 @@ export class AllApplicationsComponent implements OnInit { debounceTime(500), switchMap(async (report) => { if (report && report.organization?.id && report.data && report.summary) { - const data = await this.reportService.generateRiskInsightsReport( + const data = await this.reportService.generateEncryptedRiskInsightsReport( report.organization.id as OrganizationId, report.data, report.summary, @@ -188,10 +248,9 @@ export class AllApplicationsComponent implements OnInit { request, ), ); - // console.log("Risk insights report saved successfully.", JSON.stringify(response)); return response; } catch { - // console.error("Error saving risk insights report:", error); + /* continue as usual */ } return null; diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts index ab74869bfc9..020b8c5380d 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/risk-insights.component.ts @@ -113,7 +113,7 @@ export class RiskInsightsComponent implements OnInit { switchMap((orgId: string | null) => { if (orgId) { this.organizationId = orgId; - this.dataService.fetchApplicationsReport(orgId); + // this.dataService.fetchApplicationsReport(orgId); this.isLoading$ = this.dataService.isLoading$; this.isRefreshing$ = this.dataService.isRefreshing$; this.dataLastUpdated$ = this.dataService.dataLastUpdated$;