Skip to content

[PM-20577] Encrypt Risk Insights report and send it to server #14659

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

voommen-livefront
Copy link
Collaborator

@voommen-livefront voommen-livefront commented May 6, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-20577

📔 Objective

Encrypt At risk reports data and send it to the server to save

📸 Screenshots

image

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@voommen-livefront voommen-livefront requested a review from a team as a code owner May 6, 2025 21:27
@voommen-livefront voommen-livefront marked this pull request as draft May 6, 2025 21:27
Copy link

codecov bot commented May 6, 2025

Codecov Report

Attention: Patch coverage is 14.28571% with 66 lines in your changes missing coverage. Please review.

Project coverage is 36.33%. Comparing base (855dad7) to head (45212c2).
Report is 59 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../access-intelligence/all-applications.component.ts 0.00% 28 Missing ⚠️
...-insights/services/risk-insights-report.service.ts 18.18% 27 Missing ⚠️
...isk-insights/services/risk-insights-api.service.ts 41.66% 7 Missing ⚠️
...ts/risk-insights/services/critical-apps.service.ts 0.00% 2 Missing ⚠️
...n/src/dirt/reports/risk-insights/services/index.ts 0.00% 1 Missing ⚠️
.../access-intelligence/access-intelligence.module.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #14659      +/-   ##
==========================================
- Coverage   36.38%   36.33%   -0.06%     
==========================================
  Files        3194     3191       -3     
  Lines       92647    92221     -426     
  Branches    16674    16535     -139     
==========================================
- Hits        33712    33508     -204     
+ Misses      56511    56322     -189     
+ Partials     2424     2391      -33     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

github-actions bot commented May 6, 2025

Logo
Checkmarx One – Scan Summary & Detailsed3c7c0d-12fd-450f-a4a9-43040671fd4b

New Issues (8)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CVE-2025-24010 Npm-vite-5.4.6
detailsRecommended version: 5.4.19
Description: Vite is a frontend tooling framework for JavaScript. Vite allowed any websites to send any requests to the development server and read the response...
Attack Vector: NETWORK
Attack Complexity: LOW

ID: t6X%2B6Ind04Xdf1vq7kzERO5SemA8EO10QvSL6jReW10%3D
Vulnerable Package
MEDIUM CVE-2025-27789 Npm-@babel/helpers-7.26.9
detailsRecommended version: 7.26.10
Description: Babel is a compiler for writing next-generation JavaScript. In affected versions of Babel, to compile regular expressions named capturing groups, B...
Attack Vector: LOCAL
Attack Complexity: LOW

ID: KBeW2MSr3wysYMpHNsJPpnEhvgROYloA91WAigXo5ls%3D
Vulnerable Package
MEDIUM CVE-2025-30208 Npm-vite-5.4.6
detailsRecommended version: 5.4.19
Description: Vite, a provider of frontend development tooling, has a vulnerability in versions through 4.5.9, 5.0.0 through 5.4.14, 6.0.0 through 6.0.11, 6.1.0 ...
Attack Vector: NETWORK
Attack Complexity: HIGH

ID: HL29PKtDLhyuU6YnROfTGbW5GaflaYXKDAR92xK5Kjw%3D
Vulnerable Package
MEDIUM CVE-2025-31125 Npm-vite-5.4.6
detailsRecommended version: 5.4.19
Description: Vite is a frontend tooling framework for javascript. Vite exposes the content of non-allowed files using `?inline&import` or `?raw?import`. Only ap...
Attack Vector: NETWORK
Attack Complexity: HIGH

ID: %2FeH2Me3TDSrQm0rSL1E26sB2UOF%2F3wI170Qt%2BbrDGbw%3D
Vulnerable Package
MEDIUM CVE-2025-31486 Npm-vite-5.4.6
detailsRecommended version: 5.4.19
Description: A vulnerability in Vite allows the contents of arbitrary files to be returned to the browser. By appending "?.svg" along with "?.wasm?init" or sett...
Attack Vector: NETWORK
Attack Complexity: HIGH

ID: bng8j5MvHF61VKPtuTpCMz9RwcI3ktpOvJKvZn%2BiF8Y%3D
Vulnerable Package
MEDIUM CVE-2025-32395 Npm-vite-5.4.6
detailsRecommended version: 5.4.19
Description: Vite is a frontend tooling framework for JavaScript. The contents of arbitrary files can be returned to the browser if the dev server is running on...
Attack Vector: NETWORK
Attack Complexity: LOW

ID: hhRCNorrEgYCsFRsA2Cu8TjCTmH%2Bla3IGkyztcmFQKA%3D
Vulnerable Package
MEDIUM CVE-2025-46565 Npm-vite-5.4.6
detailsRecommended version: 5.4.19
Description: Vite is a frontend tooling framework for javascript. In vite package versions through 4.5.13, 5.0.0-beta.0 through 5.4.18, 6.0.0-alpha.0 through 6....
Attack Vector: NETWORK
Attack Complexity: LOW

ID: yyS5g51Ots80gaD5DqC6imeLYLbDGHwliwpoHr%2BQ61o%3D
Vulnerable Package
MEDIUM Client_DOM_Open_Redirect /apps/desktop/src/auth/accessibility-cookie.component.html: 18
detailsThe potentially tainted value provided by link in /apps/desktop/src/auth/accessibility-cookie.component.html at line 18 is used as a destination...
ID: n8z%2BtT3KdiqSNpDTrTSQQymGG%2BY%3D
Attack Vector

Copy link
Contributor

@quexten quexten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, while I'm not a code owner here, this uses KM APIs and increases KM tech debt, and overlaps with upcoming KM work on org key rotation. My main goal here is to use content encryption keys for the reports, as we have been doing with e.g. ciphers (cipherkeys) and attachments (attachment keys) to not put us in a situation where we have to download and re-upload all reports.

Looking at this though, the unencrypted metadata caught my eye. This may introduce unintended issues, and I'll create a slack thread on this.

details: ApplicationHealthReportDetail[],
summary: ApplicationHealthReportSummary,
): Promise<RiskInsightsReport> {
const key = await this.keyService.getOrgKey(organizationId as string);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ I realize this PR is still in draft, but I imagine there are related PR's (server), so I'm already commenting this. Using the org key here directly both has security implications, but also cryptographic tech debt implications when considering organization key rotation. If we want to rotate the org key, that means downloading and uploading all reports.

Can we please not directly encrypt with the org key here, but use a layer of indirection? This way, the encryption scope is limited, and also key rotation becomes just re-uploading the content-encryption-keys, not the content. This is a pattern we use with attachments and ciphers (featureflagged still).

Specifically something like:

const orgKey = await this.keyService.getOrgKey(...);
const reportContentEncryptionKey = await this.keyGenerationService.createKey(512);
const wrappedReportContentEncryptionKey = await this.encryptService.wrapSymmetricKey(reportContentEncryptionKey, orgKey);

// now save "wrappedReportContentEncryptionKey" along with reportData (encrypted by `reportContentEncryptionkey`)

I realize this is not obvious from the interfaces yet, and the interfaces KM offers here are in-flight / changing. If you have questions / need assistance please feel free to reach out to key-management in slack.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even nicer would be to introduce an intermediate key here, a "risk insights key", that would wrap all reportContentEncryptionKeys, and itself be wrapped by the org key. But I don't want to increase scope more than necessary.

organizationId: OrganizationId;
date: string;
reportData: string;
totalMembers: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question ❓ Will this report be automatically generated and submitted on a schedule, or manually created only? If the former, are we OK with this unencrypted metadata?

⚠️⚠️⚠️ Assuming it can be run automated: I don't know the underlying security guarantees that are expected to be achieved here. From the fact that we encrypt reports, but do not encrypt summaries, I assume the underlying assumption here is that summaries do not leak individual data so it's "safe to store". That (assumed) assumption is not valid.

Unless not possible, I would recommend encrypting all metadata here. Specifically, reportData should be one blob including both the summary data and detail data. Below, I show a theoretical attack that may abuse the summary data to decrypt all org vault data remotely.

Specifically, the following attacks (non exhaustive, and non validated) may be made possible. I have not implemented or validated them, and they are purely on the basis of reading the code.

  • Assumption: There is a automated schedule / way to trigger the generation
  • Threat model: the attacker controls the API server.

Leak individiual item/application weakness status

Before every report generation, the server forces a sync to the client that will create the report, and present a false view of the ciphers, specifically, just show 1 cipher. The total will then reflect just that cipher, and thus the safety presumed by the use of summarized data is broken. This attack can be optimized to be much faster than a linear amount of queries.

Remote predicate evaluation on arbitrary org encrypted strings

Further, it allows the server to remotely run certain predicates such as findWeakPassword on any arbitrary org-key encrypted data (not just passwords) and obtain the result, by swapping encrypted strings within the cipher.

  • Possible full plaintext recovery, breaking organization e2e encryption: Doing a brief evaluation, without further validation, this.passwordStrengthService.getPasswordStrength( is called, which uses zxcvbn to compare a security score, of the password compared to, including the encrypted "username" field. If the server has access to known plaintext ciphertext pairs here, they may be able to create a decryption oracle capable of remotely decrypting all org vault data with this. NOTE: This is theoretical, I have not validated this thoroughly. Roughly, the attacker would remotely, via the previous attack make the client evaluate password strength. As the password, the server sets the target encstring. As the username, they set one of the known plaintext / ciphertext pairs. zxcvbn will return a different score based on the similarity of the two, and depending on if it reaches a threshold, it will be a cipher at risk. This can be used, given enough plaintext ciphertext pairs to fully recovery the target plaintext, thus making full decryption of all vault data possible. One would find a plaintext-ciphertext pair close to the threshold, then flip each character to figure out which character is the correct character for that position. However, this requires a lot of plaintext ciphertext pairs, or an encryption oracle.

Remote predicate evaluation - equality check

Using the same attack as above, the server can perform an equality check on any two arbitrary org-key encrypted org strings, by presenting 2 items, with the target encstrings set as the password.

Remote clustering of encrypted vault items by app

Further, it allows the server to cluster any ciphers and confirm if the are for the same domain. Example: Server provides cipher A, cipher B. If only one app is reported, then both are the same app and get clustered. This can be improved to be much faster than linear time.

(Out of scope note) Clusters / individual weak items can be tied to real domains

Combining this with a compromised icon server (out of scope) would allow tying encrypted ciphers to domains, and thus leak which accounts are weak.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will cc @mandreko-bitwarden, @jlf0dev on this, because these (presumed) attacks are security relevant, possibly threatening all of organization vault encryption confidentiality. I'm keeping these comments public because this is not released code, but just draft.

@jlf0dev I have not implemented these attacks because I looked at this as an early code review for the unrelated issue bellow, but it felt unethical not to look closer here. I don't know if it's worth to spend the time to confirm the validity of the attacks above. If we just encrypt the metadata, then all of them are invalid etiher way.

Copy link

sonarqubecloud bot commented May 9, 2025

Copy link
Contributor

@quexten quexten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @voommen-livefront I've taken a look. Thank you for working to address the KM tech debt concern regarding the content encryption key. Sadly the APIs that KM offers are a bit confusing still and I'll bring it up within KM.

I believe this PR currently uses two layers of encryption (accidentally?) which both not necessary, but also makes the content-encryption-key useless, because now rotating the org key would again require a full re-upload of the data since the "outer layer" is encrypted with the org key. I've added a suggestion of how it could look.

const riskInsightReport = {
organizationId: organizationId,
date: new Date().toISOString(),
reportData: encryptedReportDataWithWrappedKey.encryptedString,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reportData: encryptedReportDataWithWrappedKey.encryptedString,
reportData: reportEncrypted.encryptedString,
reportKey: wrappedReportContentEncryptionKey.encryptedString,

Comment on lines +198 to +206
const reportDataWithWrappedKey = {
data: reportEncrypted.encryptedString,
key: wrappedReportContentEncryptionKey.encryptedString,
};

const encryptedReportDataWithWrappedKey = await this.encryptService.encryptString(
JSON.stringify(reportDataWithWrappedKey),
orgKey,
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const reportDataWithWrappedKey = {
data: reportEncrypted.encryptedString,
key: wrappedReportContentEncryptionKey.encryptedString,
};
const encryptedReportDataWithWrappedKey = await this.encryptService.encryptString(
JSON.stringify(reportDataWithWrappedKey),
orgKey,
);

I don't think this is right. This is effectively adds a second layer of encryption around already encrypted data, wrapping all data again with the org key. As far as I can tell, you can send data, key directly to the server, without another layer of encryption because:

  • data is encrypted by reportContentEncryptionKey
  • key (reportContentEncryptionKey) is wrapped (encrypted) with orgKey

The above is basically creating: orgKey({contentKey(data),orgKey(data)})
where you just need {contentKey(data),orgKey(data)}.

Comment on lines +238 to +245
const decryptedReportDataWithWrappedKey = await this.encryptService.decryptString(
new EncString(riskInsightsReportResponse.reportData),
orgKey,
);

const reportDataInJson = JSON.parse(decryptedReportDataWithWrappedKey);
const reportEncrypted = reportDataInJson.data;
const wrappedReportContentEncryptionKey = reportDataInJson.key;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 wrappedReportContentEncryptionKey = riskInsightsReportResponse.reportKey;
const reportEncrypted = riskInsightsReportResponse.reportData;

With the same reasoning as above, I don't think we need two layers of encryption here?

throw new Error("Organization key not found");
}

const reportContentEncryptionKey = await this.keyGeneratorService.createKey(512);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note/thought (non-actionable): I believe this is a pattern that other teams (in this case DIRT) have to implement over and over, and should really live in KM domain.

So, specifically, the use-case is "I want to encrypt blobs of data (attachment/report/cipherblob/etc.) safely, and be able to easily update the encryption".

I believe in the future KM should/may provide a high level API similar to:

Encrypt:

const { wrappedContentEncryptionKey, sealedData } = this.encryptService.sealDataEnvelope(MyReport, orgKey);

Decrypt:

const { unsealedData } = this.encryptService.unsealDataEnvelope(sealedData, wrappedContentEncryptionKey, orgKey);

Which would eliminate most cryptographic code that this team (dirt) is forced to currently think about, but at the same time enforce correct usage of a content encryption key.

@Thomas-Avery do we want to discuss this in KM dev sync first or do I make a tech debt ticket for us to provide a better interface, and ask for the ticket to be put in this section of code to serve as a reminder?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants