Fix: SSH credential public key decryption across browser sessions #342

Merged
thorved merged 1 commits from fix/ssh-credential-public-key-decryption into dev-1.8.0 2025-10-05 00:09:10 +00:00
thorved commented 2025-10-04 06:22:39 +00:00 (Migrated from github.com)

Fix SSH Credential Public Key Decryption Issue

🐛 Problem Description

User Experience Issue

When a user adds SSH credentials (with SSH key authentication) in one browser session, everything works perfectly - they can view the credentials and connect to SSH terminals without any issues. However, when the same user logs in from a different browser, a critical bug appears:

What the user sees:

  • Private Key: Displays correctly (decrypted, readable SSH key format)
  • Public Key: Shows as raw encrypted JSON instead of the actual public key

Example of the broken public key display:

{"data":"ccb71dc2de47428330c8ebb7586103dc1557dc67d81f2363a892aa133f118d186fdd4f1ff725ec05e3e262604103b098...","iv":"7bfe6726abc3c7982757e96b371f0383","tag":"3c71b159baf347582a4c18295409d16f","salt":"6bfab4ced2e150a25c7532cd541b8fb133d6aef5225c48f85ae24307c7475e17","recordId":"2"}

See the screenshot showing the issue - the private key displays properly while the public key shows encrypted JSON.

Impact

  • Server stats continue to work fine (they use the private key internally)
  • SSH terminal connections fail because the system cannot properly use the public key for authentication
  • Users experience inconsistent behavior across different browser sessions
  • The credential appears "broken" when accessed from a new browser, even though it works in the original browser
  • Creates confusion and frustration for users who expect credentials to work consistently

Reproduction Steps

  1. Create SSH credentials with an SSH key in Browser A (e.g., Chrome)
  2. Save the credentials and verify terminal connection works
  3. Log out from Browser A
  4. Log in to the same account from Browser B (e.g., Firefox)
  5. Navigate to Credentials and view the SSH credential
  6. Bug appears: Public key shows as encrypted JSON instead of the actual key
  7. Attempt to open a terminal with that credential fails

🔍 Root Cause

The issue was a field name mismatch in the encryption/decryption system:

  1. Drizzle ORM returns query results with camelCase property names (e.g., publicKey, privateKey, keyPassword) based on the schema definition in src/backend/database/db/schema.ts:

    publicKey: text("public_key", { length: 4096 })
    privateKey: text("private_key", { length: 16384 })
    
  2. The ENCRYPTED_FIELDS configuration in src/backend/utils/field-crypto.ts only listed snake_case field names (e.g., public_key, private_key, key_password)

  3. When DataCrypto.decryptRecord() checked if a field should be decrypted, it looked for the camelCase property name (publicKey) in a Set that only contained the snake_case name (public_key)

  4. The check failed → shouldEncryptField() returned false → field was not decrypted → raw encrypted JSON was returned to the frontend

Technical Flow

Database (SQLite)          Drizzle ORM              Decryption Check           Result
─────────────────          ───────────              ────────────────           ──────
public_key column    →     publicKey property  →    Is "publicKey" in Set?  →  FALSE!
(encrypted JSON)           (still encrypted)        ["public_key", ...]        (skipped)
                                                                            ↓
                                                                      Raw JSON returned

Why did this only affect cross-browser sessions?

The issue always existed, but wasn't immediately visible because:

  • The original browser session might have had different caching behavior
  • Some code paths had fallback logic checking both naming conventions
  • The bug only manifested when fetching credentials fresh from the database

Solution

Updated src/backend/utils/field-crypto.ts to include both camelCase and snake_case versions of all encrypted field names in the ENCRYPTED_FIELDS configuration:

Before:

ssh_credentials: new Set([
  "password",
  "private_key",    // Only snake_case
  "key_password",   // Only snake_case
  "key",
  "public_key",     // Only snake_case
]),

After:

ssh_credentials: new Set([
  "password",
  "private_key",
  "privateKey",     // ← Added camelCase version
  "key_password",
  "keyPassword",    // ← Added camelCase version
  "key",
  "public_key",
  "publicKey",      // ← Added camelCase version
]),

This ensures that:

  • Fields are decrypted correctly when returned by Drizzle ORM queries (camelCase properties)
  • Fields are decrypted correctly when returned by raw SQL queries (snake_case columns)
  • Backward compatibility is maintained with existing data
  • SSH credentials work consistently across all browser sessions
  • Both public and private keys display properly in all scenarios

Additional Fix in lazy-field-encryption.ts

Also updated the legacy field name mapping to work bidirectionally, ensuring proper fallback handling for both naming conventions.

🧪 Testing

Manual Testing Steps

After this fix:

  1. Create SSH credentials with a key in Browser A
  2. Verify the credential displays both private and public keys correctly
  3. Log in from Browser B
  4. View the same credential
  5. Both private key and public key should display properly (not as encrypted JSON)
  6. Open an SSH terminal connection - should work correctly
  7. Server stats should continue to function normally

Expected Behavior

  • Public key displays as proper SSH public key format (e.g., ssh-rsa AAAAB3NzaC1...)
  • Private key displays as proper OpenSSH private key format (with BEGIN/END markers)
  • No encrypted JSON visible in the UI
  • Terminal connections work from any browser session

📝 Changed Files

  • src/backend/utils/field-crypto.ts - Updated ENCRYPTED_FIELDS to include both camelCase and snake_case naming conventions
  • src/backend/utils/lazy-field-encryption.ts - Enhanced legacy field name mapping for bidirectional support

Fixes the critical issue where SSH public keys displayed as encrypted JSON when users logged in from different browsers, preventing terminal connections from working and causing user confusion.

📸 Visual Evidence

The attached screenshot shows the bug - notice how the SSH Public Key field displays the encrypted JSON structure instead of the actual public key, while the SSH Private Key displays correctly.

# Fix SSH Credential Public Key Decryption Issue ## 🐛 Problem Description ### User Experience Issue When a user adds SSH credentials (with SSH key authentication) in one browser session, everything works perfectly - they can view the credentials and connect to SSH terminals without any issues. However, when the **same user logs in from a different browser**, a critical bug appears: **What the user sees:** - ✅ **Private Key**: Displays correctly (decrypted, readable SSH key format) - ❌ **Public Key**: Shows as raw encrypted JSON instead of the actual public key **Example of the broken public key display:** ```json {"data":"ccb71dc2de47428330c8ebb7586103dc1557dc67d81f2363a892aa133f118d186fdd4f1ff725ec05e3e262604103b098...","iv":"7bfe6726abc3c7982757e96b371f0383","tag":"3c71b159baf347582a4c18295409d16f","salt":"6bfab4ced2e150a25c7532cd541b8fb133d6aef5225c48f85ae24307c7475e17","recordId":"2"} ``` See the screenshot showing the issue - the private key displays properly while the public key shows encrypted JSON. ### Impact - **Server stats** continue to work fine (they use the private key internally) - **SSH terminal connections fail** because the system cannot properly use the public key for authentication - Users experience inconsistent behavior across different browser sessions - The credential appears "broken" when accessed from a new browser, even though it works in the original browser - Creates confusion and frustration for users who expect credentials to work consistently ### Reproduction Steps 1. Create SSH credentials with an SSH key in Browser A (e.g., Chrome) 2. Save the credentials and verify terminal connection works ✅ 3. Log out from Browser A 4. Log in to the same account from Browser B (e.g., Firefox) 5. Navigate to Credentials and view the SSH credential 6. **Bug appears**: Public key shows as encrypted JSON instead of the actual key ❌ 7. Attempt to open a terminal with that credential fails ## 🔍 Root Cause The issue was a **field name mismatch** in the encryption/decryption system: 1. **Drizzle ORM** returns query results with **camelCase** property names (e.g., `publicKey`, `privateKey`, `keyPassword`) based on the schema definition in `src/backend/database/db/schema.ts`: ```typescript publicKey: text("public_key", { length: 4096 }) privateKey: text("private_key", { length: 16384 }) ``` 2. The `ENCRYPTED_FIELDS` configuration in `src/backend/utils/field-crypto.ts` only listed **snake_case** field names (e.g., `public_key`, `private_key`, `key_password`) 3. When `DataCrypto.decryptRecord()` checked if a field should be decrypted, it looked for the camelCase property name (`publicKey`) in a Set that only contained the snake_case name (`public_key`) 4. The check failed → `shouldEncryptField()` returned `false` → field was **not decrypted** → raw encrypted JSON was returned to the frontend ### Technical Flow ``` Database (SQLite) Drizzle ORM Decryption Check Result ───────────────── ─────────── ──────────────── ────── public_key column → publicKey property → Is "publicKey" in Set? → FALSE! (encrypted JSON) (still encrypted) ["public_key", ...] (skipped) ↓ Raw JSON returned ``` ### Why did this only affect cross-browser sessions? The issue always existed, but wasn't immediately visible because: - The original browser session might have had different caching behavior - Some code paths had fallback logic checking both naming conventions - The bug only manifested when fetching credentials fresh from the database ## ✅ Solution Updated `src/backend/utils/field-crypto.ts` to include **both camelCase and snake_case** versions of all encrypted field names in the `ENCRYPTED_FIELDS` configuration: **Before:** ```typescript ssh_credentials: new Set([ "password", "private_key", // Only snake_case "key_password", // Only snake_case "key", "public_key", // Only snake_case ]), ``` **After:** ```typescript ssh_credentials: new Set([ "password", "private_key", "privateKey", // ← Added camelCase version "key_password", "keyPassword", // ← Added camelCase version "key", "public_key", "publicKey", // ← Added camelCase version ]), ``` This ensures that: - ✅ Fields are decrypted correctly when returned by Drizzle ORM queries (camelCase properties) - ✅ Fields are decrypted correctly when returned by raw SQL queries (snake_case columns) - ✅ Backward compatibility is maintained with existing data - ✅ SSH credentials work consistently across all browser sessions - ✅ Both public and private keys display properly in all scenarios ### Additional Fix in `lazy-field-encryption.ts` Also updated the legacy field name mapping to work bidirectionally, ensuring proper fallback handling for both naming conventions. ## 🧪 Testing ### Manual Testing Steps After this fix: 1. ✅ Create SSH credentials with a key in Browser A 2. ✅ Verify the credential displays both private and public keys correctly 3. ✅ Log in from Browser B 4. ✅ View the same credential 5. ✅ **Both private key and public key should display properly** (not as encrypted JSON) 6. ✅ Open an SSH terminal connection - should work correctly 7. ✅ Server stats should continue to function normally ### Expected Behavior - Public key displays as proper SSH public key format (e.g., `ssh-rsa AAAAB3NzaC1...`) - Private key displays as proper OpenSSH private key format (with BEGIN/END markers) - No encrypted JSON visible in the UI - Terminal connections work from any browser session ## 📝 Changed Files - `src/backend/utils/field-crypto.ts` - Updated `ENCRYPTED_FIELDS` to include both camelCase and snake_case naming conventions - `src/backend/utils/lazy-field-encryption.ts` - Enhanced legacy field name mapping for bidirectional support ## 🔗 Related Issues Fixes the critical issue where SSH public keys displayed as encrypted JSON when users logged in from different browsers, preventing terminal connections from working and causing user confusion. ## 📸 Visual Evidence The attached screenshot shows the bug - notice how the SSH Public Key field displays the encrypted JSON structure instead of the actual public key, while the SSH Private Key displays correctly.
gemini-code-assist[bot] commented 2025-10-04 06:22:50 +00:00 (Migrated from github.com)

Summary of Changes

Hello @thorved, I'm Gemini Code Assist1 ! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical bug that prevented SSH public keys from being correctly decrypted and displayed when a user accessed their credentials from a new browser session. Previously, the public key would appear as encrypted JSON, leading to failed SSH terminal connections. The solution involves updating the encryption system's field name configurations to properly recognize both camelCase and snake_case property names, ensuring consistent and correct decryption of SSH credentials across all browser sessions.

Highlights

  • Critical Bug Fix: Addresses an issue where SSH public keys failed to decrypt and displayed as raw encrypted JSON when accessed from a different browser session, preventing SSH terminal connections.
  • Root Cause Identified: The problem stemmed from a field name mismatch in the encryption/decryption system; Drizzle ORM returned camelCase property names, but the ENCRYPTED_FIELDS configuration only contained snake_case names, causing decryption to be skipped for affected fields.
  • Configuration Update: The ENCRYPTED_FIELDS configuration in src/backend/utils/field-crypto.ts has been updated to include both camelCase and snake_case versions of all relevant encrypted field names, ensuring proper decryption regardless of the naming convention used by the ORM.
  • Bidirectional Mapping Enhancement: An additional fix in src/backend/utils/lazy-field-encryption.ts enhances legacy field name mapping to work bidirectionally, further improving compatibility and robustness.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.


  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩︎

## Summary of Changes Hello @thorved, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves a critical bug that prevented SSH public keys from being correctly decrypted and displayed when a user accessed their credentials from a new browser session. Previously, the public key would appear as encrypted JSON, leading to failed SSH terminal connections. The solution involves updating the encryption system's field name configurations to properly recognize both camelCase and snake_case property names, ensuring consistent and correct decryption of SSH credentials across all browser sessions. ### Highlights * **Critical Bug Fix**: Addresses an issue where SSH public keys failed to decrypt and displayed as raw encrypted JSON when accessed from a different browser session, preventing SSH terminal connections. * **Root Cause Identified**: The problem stemmed from a field name mismatch in the encryption/decryption system; Drizzle ORM returned camelCase property names, but the `ENCRYPTED_FIELDS` configuration only contained snake_case names, causing decryption to be skipped for affected fields. * **Configuration Update**: The `ENCRYPTED_FIELDS` configuration in `src/backend/utils/field-crypto.ts` has been updated to include both camelCase and snake_case versions of all relevant encrypted field names, ensuring proper decryption regardless of the naming convention used by the ORM. * **Bidirectional Mapping Enhancement**: An additional fix in `src/backend/utils/lazy-field-encryption.ts` enhances legacy field name mapping to work bidirectionally, further improving compatibility and robustness. <details> <summary><b>Using Gemini Code Assist</b></summary> <br> The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips. <b>Invoking Gemini</b> You can request assistance from Gemini at any point by creating a comment using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands on the current page. Feature | Command | Description --- | --- | --- Code Review | `/gemini review` | Performs a code review for the current pull request in its current state. Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state. Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in pull request comments and review comments. Help | `/gemini help` | Displays a list of available commands. <b>Customization</b> To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github). <b>Limitations & Feedback</b> Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up [here](https://google.qualtrics.com/jfe/form/SV_2cyuGuTWsEw84yG). <b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b> </details> [^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).
gemini-code-assist[bot] (Migrated from github.com) reviewed 2025-10-04 06:24:18 +00:00
gemini-code-assist[bot] (Migrated from github.com) left a comment

Code Review

This pull request effectively addresses a critical bug where SSH public keys were not being decrypted correctly across different browser sessions. The root cause, a mismatch between camelCase property names from the ORM and snake_case names in the encryption configuration, is well-identified. The solution of adding both naming conventions to the configuration is a direct fix. I've added a couple of suggestions to improve the long-term maintainability of this solution by reducing code duplication and making the configuration less error-prone.

## Code Review This pull request effectively addresses a critical bug where SSH public keys were not being decrypted correctly across different browser sessions. The root cause, a mismatch between `camelCase` property names from the ORM and `snake_case` names in the encryption configuration, is well-identified. The solution of adding both naming conventions to the configuration is a direct fix. I've added a couple of suggestions to improve the long-term maintainability of this solution by reducing code duplication and making the configuration less error-prone.
@@ -32,3 +44,4 @@
"publicKey",
]),
};
gemini-code-assist[bot] (Migrated from github.com) commented 2025-10-04 06:24:18 +00:00

medium

While adding both snake_case and camelCase versions of the field names fixes the issue, it introduces duplication and makes maintenance more error-prone. A developer might forget to add both versions for a new encrypted field in the future.

A more robust and maintainable approach would be to keep only the canonical snake_case names in ENCRYPTED_FIELDS and handle the case conversion inside the shouldEncryptField method. This would centralize the logic and keep the configuration clean.

For example, shouldEncryptField could convert the incoming fieldName to snake_case before checking for its existence in the set. This would work for both camelCase and snake_case inputs and would allow you to revert this part of the change, simplifying the configuration.

![medium](https://www.gstatic.com/codereviewagent/medium-priority.svg) While adding both `snake_case` and `camelCase` versions of the field names fixes the issue, it introduces duplication and makes maintenance more error-prone. A developer might forget to add both versions for a new encrypted field in the future. A more robust and maintainable approach would be to keep only the canonical `snake_case` names in `ENCRYPTED_FIELDS` and handle the case conversion inside the `shouldEncryptField` method. This would centralize the logic and keep the configuration clean. For example, `shouldEncryptField` could convert the incoming `fieldName` to `snake_case` before checking for its existence in the set. This would work for both `camelCase` and `snake_case` inputs and would allow you to revert this part of the change, simplifying the configuration.
LukeGus commented 2025-10-04 06:54:10 +00:00 (Migrated from github.com)

Well, that explains that issue. I'll merge this tomorrow when I have time, since it's 2 am right now. I will probably try to do a 1.7.2 release tomorrow just to get this fixed. Thank you.

Well, that explains that issue. I'll merge this tomorrow when I have time, since it's 2 am right now. I will probably try to do a 1.7.2 release tomorrow just to get this fixed. Thank you.
Sign in to join this conversation.