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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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$;
|
||||
|
||||
Reference in New Issue
Block a user