1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 12:40:26 +00:00

PM-20577 updated encryption/decryption

This commit is contained in:
voommen-livefront
2025-05-09 12:03:30 -05:00
parent d4271b49d8
commit 9319dbcc90
7 changed files with 186 additions and 25 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<SaveRiskInsightsReportResponse>);
}
getRiskInsightsReport(orgId: OrganizationId): Observable<GetRiskInsightsReportResponse> {
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<GetRiskInsightsReportResponse>);
}
return from(dbResponse as Promise<GetRiskInsightsReportResponse>);
}
}

View File

@@ -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<RiskInsightsReport> {
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];
}
}
/**

View File

@@ -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,
],
},
{

View File

@@ -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;

View File

@@ -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$;