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:
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
Create SSH credentials with an SSH key in Browser A (e.g., Chrome)
Save the credentials and verify terminal connection works ✅
Log out from Browser A
Log in to the same account from Browser B (e.g., Firefox)
Navigate to Credentials and view the SSH credential
Bug appears: Public key shows as encrypted JSON instead of the actual key ❌
Attempt to open a terminal with that credential fails
🔍 Root Cause
The issue was a field name mismatch in the encryption/decryption system:
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:
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)
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)
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: newSet(["password","private_key",// Only snake_case
"key_password",// Only snake_case
"key","public_key",// Only snake_case
]),
After:
ssh_credentials: newSet(["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:
✅ Create SSH credentials with a key in Browser A
✅ Verify the credential displays both private and public keys correctly
✅ Log in from Browser B
✅ View the same credential
✅Both private key and public key should display properly (not as encrypted JSON)
✅ Open an SSH terminal connection - should work correctly
✅ 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.
# 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.
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.
## 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 16:24:18 +10:00
gemini-code-assist[bot]
(Migrated from github.com)
left a comment
Copy Link
Copy Source
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.
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.

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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
Example of the broken public key display:
See the screenshot showing the issue - the private key displays properly while the public key shows encrypted JSON.
Impact
Reproduction Steps
🔍 Root Cause
The issue was a field name mismatch in the encryption/decryption system:
Drizzle ORM returns query results with camelCase property names (e.g.,
publicKey,privateKey,keyPassword) based on the schema definition insrc/backend/database/db/schema.ts:The
ENCRYPTED_FIELDSconfiguration insrc/backend/utils/field-crypto.tsonly listed snake_case field names (e.g.,public_key,private_key,key_password)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)The check failed →
shouldEncryptField()returnedfalse→ field was not decrypted → raw encrypted JSON was returned to the frontendTechnical Flow
Why did this only affect cross-browser sessions?
The issue always existed, but wasn't immediately visible because:
✅ Solution
Updated
src/backend/utils/field-crypto.tsto include both camelCase and snake_case versions of all encrypted field names in theENCRYPTED_FIELDSconfiguration:Before:
After:
This ensures that:
Additional Fix in
lazy-field-encryption.tsAlso updated the legacy field name mapping to work bidirectionally, ensuring proper fallback handling for both naming conventions.
🧪 Testing
Manual Testing Steps
After this fix:
Expected Behavior
ssh-rsa AAAAB3NzaC1...)📝 Changed Files
src/backend/utils/field-crypto.ts- UpdatedENCRYPTED_FIELDSto include both camelCase and snake_case naming conventionssrc/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.
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
ENCRYPTED_FIELDSconfiguration only contained snake_case names, causing decryption to be skipped for affected fields.ENCRYPTED_FIELDSconfiguration insrc/backend/utils/field-crypto.tshas 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.src/backend/utils/lazy-field-encryption.tsenhances 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./gemini review/gemini summary/gemini helpCustomization
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.
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. ↩︎
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
camelCaseproperty names from the ORM andsnake_casenames 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",]),};While adding both
snake_caseandcamelCaseversions 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_casenames inENCRYPTED_FIELDSand handle the case conversion inside theshouldEncryptFieldmethod. This would centralize the logic and keep the configuration clean.For example,
shouldEncryptFieldcould convert the incomingfieldNametosnake_casebefore checking for its existence in the set. This would work for bothcamelCaseandsnake_caseinputs and would allow you to revert this part of the change, simplifying the configuration.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.