Fix SSH Key Password (keyPassword) Field Naming Mismatch Between Frontend and Backend #375

Merged
thorved merged 3 commits from fix--sshpasskey-missmatch into dev-1.7.3 2025-10-07 21:05:34 +00:00
thorved commented 2025-10-07 20:30:37 +00:00 (Migrated from github.com)

Fix SSH Key Password (keyPassword) Field Naming Mismatch Between Frontend and Backend

🐛 Problem

The application was experiencing authentication failures when using encrypted SSH private keys with passwords. Users would see the error:

Authentication failed: Failed to connect to host: Cannot parse privateKey: 
Encrypted private OpenSSH key detected, but no passphrase given

Additionally, the "Server Metrics" feature would fail to load with "Failed to fetch server metrics" error.

Root Cause

There was a field naming inconsistency between the frontend and backend:

  • Frontend: Sending and expecting keyPassword (camelCase)
  • Backend: Expecting and returning key_password (snake_case)
  • Database: Column named key_password (snake_case)

This mismatch caused the SSH key password to not be passed correctly to the SSH2 client, resulting in connection failures.

Solution

Implemented a comprehensive field naming standardization across the entire codebase to use camelCase (keyPassword) in the API layer while maintaining snake_case (key_password) in the database layer.

📝 Changes Made

1. SSH Routes (src/backend/database/routes/ssh.ts)

  • Updated POST /ssh/db/host to accept keyPassword from frontend
  • Updated PUT /ssh/db/host/:id to accept keyPassword from frontend
  • Modified resolveHostCredentials() to convert key_passwordkeyPassword when returning data
  • Fixed internal endpoints to return keyPassword instead of key_password
  • Ensured both credential-based and inline credential hosts properly convert field names
  • Fixed critical bug: Prevented stale key_password from overwriting credential-resolved keyPassword
  • Fixed export endpoint to use keyPassword instead of deleted key_password field

2. Server Stats (src/backend/ssh/server-stats.ts)

  • Updated addLegacyCredentials() to read key_password from database and convert to keyPassword
  • Fixed field resolution to check host.key_password || host.keyPassword

3. Host Manager UI (src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx)

  • Fixed tab switching behavior: Removed field clearing when switching between authentication methods
  • Form now preserves all auth field values when switching tabs (password/key/credential)
  • Backend properly handles only submitting fields relevant to selected authType
  • Prevents data loss when users switch between authentication tabs without saving

3. Data Flow

Frontend (keyPassword) 
    ↓
API Routes (accepts keyPassword) 
    ↓
Database (stores as key_password) 
    ↓
Database Query (returns key_password) 
    ↓
API Response (converts to keyPassword) 
    ↓
Frontend/SSH Client (uses keyPassword as passphrase)

🐛 Additional Bug Fix

Stale Data Override Prevention

Fixed a critical bug in the resolveHostCredentials() function where a host using a credential ID could have its credential's keyPassword incorrectly overwritten by a stale key_password property from the host object itself.

Scenario:

  1. Host originally created with inline credentials (had key_password)
  2. Host later updated to use a credential ID
  3. Old key_password remained in the host record
  4. When fetching, the stale key_password would override the credential's keyPassword

Solution:

// Only use the inline key_password if keyPassword hasn't been set by credential resolution
if (result.keyPassword === undefined) {
  result.keyPassword = host.key_password;
}
// Always remove the snake_case version to standardize the output
delete result.key_password;

This ensures credential-resolved keyPassword always takes precedence over stale inline values.

🧪 Tested Scenarios

  • Creating new host with encrypted SSH key + password
  • Updating existing host with encrypted SSH key + password
  • Terminal connection with encrypted SSH key
  • File Manager connection with encrypted SSH key
  • Server Stats/Metrics with encrypted SSH key
  • Hosts using credential IDs
  • Hosts using inline credentials
  • Hosts switching from inline to credential-based auth (stale data handling)
  • Auto-start tunnel functionality
  • Switching between authentication tabs (password/key/credential) without data loss
  • Export functionality preserving keyPassword field

🔧 Technical Details

Field Conversion Strategy

  • API Input: Accept keyPassword (camelCase) from frontend
  • Database Storage: Store as key_password (snake_case) in database columns
  • API Output: Convert key_passwordkeyPassword before sending to frontend
  • SSH Connection: Use keyPassword as passphrase for SSH2 client
  • Precedence: Credential-resolved values > inline values (prevents stale data override)

Files Modified

  1. src/backend/database/routes/ssh.ts

    • Line ~229: Changed parameter destructuring from key_password to keyPassword (POST)
    • Line ~281: Changed assignment to use keyPassword instead of key_password
    • Line ~410: Changed parameter destructuring from key_password to keyPassword (PUT)
    • Line ~467: Changed condition and assignment to use keyPassword
    • Line ~94: Fixed internal autostart endpoint to return keyPassword
    • Line ~154: Fixed internal all hosts endpoint to return keyPassword
    • Line ~1243-1252: Added smart conversion logic in resolveHostCredentials() to prevent stale data override
  2. src/backend/ssh/server-stats.ts

    • Line ~480: Updated addLegacyCredentials() to convert key_password to keyPassword

Backward Compatibility

  • Handles both key_password and keyPassword during reads (fallback logic)
  • Existing encrypted data continues to work
  • No database migration required
  • Terminal, File Manager, and Credentials routes already had proper handling
  • Gracefully handles stale data from configuration changes

📊 Impact

  • Before: Users couldn't connect to hosts with encrypted SSH keys that require a password
  • After: Full support for encrypted SSH keys with passwords across all features
  • Before: Stale inline credentials could override credential-based auth
  • After: Credential-based auth always takes precedence, preventing data corruption

🎯 Fixes

  • SSH key password field now saves correctly
  • Encrypted SSH keys with passwords work in Terminal
  • Encrypted SSH keys with passwords work in File Manager
  • Server Metrics now loads correctly for all hosts
  • Consistent API contract between frontend and backend
  • Stale data no longer overrides credential-resolved values
  • Proper precedence when hosts switch authentication methods

Fixes: Authentication failures with encrypted SSH keys
Fixes: "Failed to fetch server metrics" error
Fixes: Stale inline credentials overriding credential-based auth


Breaking Changes: None

Rollback Plan: Revert commits in this PR if issues arise

Testing: Manual testing completed for all SSH connection scenarios including edge cases

# Fix SSH Key Password (keyPassword) Field Naming Mismatch Between Frontend and Backend ## 🐛 Problem The application was experiencing authentication failures when using encrypted SSH private keys with passwords. Users would see the error: ``` Authentication failed: Failed to connect to host: Cannot parse privateKey: Encrypted private OpenSSH key detected, but no passphrase given ``` Additionally, the "Server Metrics" feature would fail to load with "Failed to fetch server metrics" error. ### Root Cause There was a field naming inconsistency between the frontend and backend: - **Frontend**: Sending and expecting `keyPassword` (camelCase) - **Backend**: Expecting and returning `key_password` (snake_case) - **Database**: Column named `key_password` (snake_case) This mismatch caused the SSH key password to not be passed correctly to the SSH2 client, resulting in connection failures. ## ✅ Solution Implemented a comprehensive field naming standardization across the entire codebase to use **camelCase** (`keyPassword`) in the API layer while maintaining **snake_case** (`key_password`) in the database layer. ## 📝 Changes Made ### 1. **SSH Routes** (`src/backend/database/routes/ssh.ts`) - ✅ Updated POST `/ssh/db/host` to accept `keyPassword` from frontend - ✅ Updated PUT `/ssh/db/host/:id` to accept `keyPassword` from frontend - ✅ Modified `resolveHostCredentials()` to convert `key_password` → `keyPassword` when returning data - ✅ Fixed internal endpoints to return `keyPassword` instead of `key_password` - ✅ Ensured both credential-based and inline credential hosts properly convert field names - ✅ **Fixed critical bug**: Prevented stale `key_password` from overwriting credential-resolved `keyPassword` - ✅ Fixed export endpoint to use `keyPassword` instead of deleted `key_password` field ### 2. **Server Stats** (`src/backend/ssh/server-stats.ts`) - ✅ Updated `addLegacyCredentials()` to read `key_password` from database and convert to `keyPassword` - ✅ Fixed field resolution to check `host.key_password || host.keyPassword` ### 3. **Host Manager UI** (`src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx`) - ✅ **Fixed tab switching behavior**: Removed field clearing when switching between authentication methods - ✅ Form now preserves all auth field values when switching tabs (password/key/credential) - ✅ Backend properly handles only submitting fields relevant to selected `authType` - ✅ Prevents data loss when users switch between authentication tabs without saving ### 3. **Data Flow** ``` Frontend (keyPassword) ↓ API Routes (accepts keyPassword) ↓ Database (stores as key_password) ↓ Database Query (returns key_password) ↓ API Response (converts to keyPassword) ↓ Frontend/SSH Client (uses keyPassword as passphrase) ``` ## 🐛 Additional Bug Fix ### Stale Data Override Prevention Fixed a critical bug in the `resolveHostCredentials()` function where a host using a credential ID could have its credential's `keyPassword` incorrectly overwritten by a stale `key_password` property from the host object itself. **Scenario:** 1. Host originally created with inline credentials (had `key_password`) 2. Host later updated to use a credential ID 3. Old `key_password` remained in the host record 4. When fetching, the stale `key_password` would override the credential's `keyPassword` **Solution:** ```typescript // Only use the inline key_password if keyPassword hasn't been set by credential resolution if (result.keyPassword === undefined) { result.keyPassword = host.key_password; } // Always remove the snake_case version to standardize the output delete result.key_password; ``` This ensures credential-resolved `keyPassword` always takes precedence over stale inline values. ## 🧪 Tested Scenarios - ✅ Creating new host with encrypted SSH key + password - ✅ Updating existing host with encrypted SSH key + password - ✅ Terminal connection with encrypted SSH key - ✅ File Manager connection with encrypted SSH key - ✅ Server Stats/Metrics with encrypted SSH key - ✅ Hosts using credential IDs - ✅ Hosts using inline credentials - ✅ Hosts switching from inline to credential-based auth (stale data handling) - ✅ Auto-start tunnel functionality - ✅ Switching between authentication tabs (password/key/credential) without data loss - ✅ Export functionality preserving keyPassword field ## 🔧 Technical Details ### Field Conversion Strategy - **API Input**: Accept `keyPassword` (camelCase) from frontend - **Database Storage**: Store as `key_password` (snake_case) in database columns - **API Output**: Convert `key_password` → `keyPassword` before sending to frontend - **SSH Connection**: Use `keyPassword` as passphrase for SSH2 client - **Precedence**: Credential-resolved values > inline values (prevents stale data override) ### Files Modified 1. `src/backend/database/routes/ssh.ts` - Line ~229: Changed parameter destructuring from `key_password` to `keyPassword` (POST) - Line ~281: Changed assignment to use `keyPassword` instead of `key_password` - Line ~410: Changed parameter destructuring from `key_password` to `keyPassword` (PUT) - Line ~467: Changed condition and assignment to use `keyPassword` - Line ~94: Fixed internal autostart endpoint to return `keyPassword` - Line ~154: Fixed internal all hosts endpoint to return `keyPassword` - Line ~1243-1252: Added smart conversion logic in `resolveHostCredentials()` to prevent stale data override 2. `src/backend/ssh/server-stats.ts` - Line ~480: Updated `addLegacyCredentials()` to convert `key_password` to `keyPassword` ### Backward Compatibility - ✅ Handles both `key_password` and `keyPassword` during reads (fallback logic) - ✅ Existing encrypted data continues to work - ✅ No database migration required - ✅ Terminal, File Manager, and Credentials routes already had proper handling - ✅ Gracefully handles stale data from configuration changes ## 📊 Impact - **Before**: Users couldn't connect to hosts with encrypted SSH keys that require a password - **After**: Full support for encrypted SSH keys with passwords across all features - **Before**: Stale inline credentials could override credential-based auth - **After**: Credential-based auth always takes precedence, preventing data corruption ## 🎯 Fixes - SSH key password field now saves correctly - Encrypted SSH keys with passwords work in Terminal - Encrypted SSH keys with passwords work in File Manager - Server Metrics now loads correctly for all hosts - Consistent API contract between frontend and backend - Stale data no longer overrides credential-resolved values - Proper precedence when hosts switch authentication methods ## 📸 Related Issues Fixes: Authentication failures with encrypted SSH keys Fixes: "Failed to fetch server metrics" error Fixes: Stale inline credentials overriding credential-based auth --- **Breaking Changes**: None **Rollback Plan**: Revert commits in this PR if issues arise **Testing**: Manual testing completed for all SSH connection scenarios including edge cases
gemini-code-assist[bot] commented 2025-10-07 20:30:52 +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 issue preventing the use of encrypted SSH keys with passwords and causing server metrics to fail. The core problem was a naming inconsistency for the SSH key password field between the frontend's camelCase expectation and the backend/database's snake_case usage. The solution standardizes the API contract to use 'keyPassword' for all interactions, implementing robust conversion logic to seamlessly translate between the API and database formats. This ensures that all SSH-related features now correctly handle encrypted keys, restoring full functionality and consistency.

Highlights

  • Problem Identification: The application experienced authentication failures with encrypted SSH private keys and 'Server Metrics' failing to load due to a field naming mismatch: frontend expected 'keyPassword' (camelCase), while the backend/database used 'key_password' (snake_case).
  • Solution Implemented: A comprehensive standardization was implemented to use 'keyPassword' (camelCase) in the API layer for both input and output, with conversion logic to store and retrieve the value as 'key_password' (snake_case) in the database.
  • Key Changes: Updates were made across SSH routes (POST/PUT) to accept 'keyPassword', modifications to 'resolveHostCredentials()' to convert 'key_password' to 'keyPassword' for API responses, and adjustments in 'addLegacyCredentials()' for server stats to correctly handle the field.
  • Extensive Testing: The fix was thoroughly tested across various scenarios including creating/updating hosts with encrypted keys, terminal/file manager connections, server stats, credential IDs, inline credentials, and auto-start tunnels, ensuring full functionality.
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 issue preventing the use of encrypted SSH keys with passwords and causing server metrics to fail. The core problem was a naming inconsistency for the SSH key password field between the frontend's camelCase expectation and the backend/database's snake_case usage. The solution standardizes the API contract to use 'keyPassword' for all interactions, implementing robust conversion logic to seamlessly translate between the API and database formats. This ensures that all SSH-related features now correctly handle encrypted keys, restoring full functionality and consistency. ### Highlights * **Problem Identification**: The application experienced authentication failures with encrypted SSH private keys and 'Server Metrics' failing to load due to a field naming mismatch: frontend expected 'keyPassword' (camelCase), while the backend/database used 'key_password' (snake_case). * **Solution Implemented**: A comprehensive standardization was implemented to use 'keyPassword' (camelCase) in the API layer for both input and output, with conversion logic to store and retrieve the value as 'key_password' (snake_case) in the database. * **Key Changes**: Updates were made across SSH routes (POST/PUT) to accept 'keyPassword', modifications to 'resolveHostCredentials()' to convert 'key_password' to 'keyPassword' for API responses, and adjustments in 'addLegacyCredentials()' for server stats to correctly handle the field. * **Extensive Testing**: The fix was thoroughly tested across various scenarios including creating/updating hosts with encrypted keys, terminal/file manager connections, server stats, credential IDs, inline credentials, and auto-start tunnels, ensuring full functionality. <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-07 20:33:05 +00:00
gemini-code-assist[bot] (Migrated from github.com) left a comment

Code Review

This is a great pull request that addresses a critical bug with SSH key passwords. The description is very clear and detailed, which is much appreciated. The changes to standardize on keyPassword at the API layer while keeping key_password at the DB layer are well-implemented across the board. I've found one potential issue in the credential resolution logic, which I've detailed in a specific comment. Otherwise, the changes look solid.

## Code Review This is a great pull request that addresses a critical bug with SSH key passwords. The description is very clear and detailed, which is much appreciated. The changes to standardize on `keyPassword` at the API layer while keeping `key_password` at the DB layer are well-implemented across the board. I've found one potential issue in the credential resolution logic, which I've detailed in a specific comment. Otherwise, the changes look solid.
@@ -1234,12 +1234,27 @@ async function resolveHostCredentials(host: any): Promise<any> {
authType: credential.auth_type || credential.authType,
gemini-code-assist[bot] (Migrated from github.com) commented 2025-10-07 20:33:05 +00:00

high

There's a potential bug in this conversion logic. If a host object uses a credentialId but also has a stale key_password property from a previous configuration, this logic will incorrectly overwrite the keyPassword from the credential with the stale key_password from the host object.

This happens because the credential resolution logic merges credential properties into the host object, but the original host.key_password is preserved. This block then unconditionally uses host.key_password if it exists.

To fix this, we should only perform the conversion from key_password to keyPassword if keyPassword hasn't already been set by the credential resolution logic.

    const result = { ...host };
    if (host.key_password !== undefined) {
      // If keyPassword is not already set by credential resolution, use the inline one.
      if (result.keyPassword === undefined) {
        result.keyPassword = host.key_password;
      }
      // Always remove the snake_case version to standardize the output.
      delete result.key_password;
    }
    return result;
![high](https://www.gstatic.com/codereviewagent/high-priority.svg) There's a potential bug in this conversion logic. If a `host` object uses a `credentialId` but also has a stale `key_password` property from a previous configuration, this logic will incorrectly overwrite the `keyPassword` from the credential with the stale `key_password` from the host object. This happens because the credential resolution logic merges credential properties into the `host` object, but the original `host.key_password` is preserved. This block then unconditionally uses `host.key_password` if it exists. To fix this, we should only perform the conversion from `key_password` to `keyPassword` if `keyPassword` hasn't already been set by the credential resolution logic. ```typescript const result = { ...host }; if (host.key_password !== undefined) { // If keyPassword is not already set by credential resolution, use the inline one. if (result.keyPassword === undefined) { result.keyPassword = host.key_password; } // Always remove the snake_case version to standardize the output. delete result.key_password; } return result; ```
thorved (Migrated from github.com) reviewed 2025-10-07 20:51:17 +00:00
@@ -1234,12 +1234,27 @@ async function resolveHostCredentials(host: any): Promise<any> {
authType: credential.auth_type || credential.authType,
thorved (Migrated from github.com) commented 2025-10-07 20:51:17 +00:00

Key Password Precedence Fix

This document summarizes the small but critical fix that prevents stale inline key_password values from overwriting credential-resolved keyPassword values when resolving SSH host credentials.

const result = { ...host };
if (host.key_password !== undefined) {
  // Only use the inline key_password if keyPassword hasn't been set by credential resolution
  if (result.keyPassword === undefined) {
    result.keyPassword = host.key_password;
  }
  // Always remove the snake_case version to standardize the output
  delete result.key_password;
}

Why this matters

  • Hosts can be migrated from inline credentials to credential-backed configurations. If the old key_password remains on the host row, it must not overwrite the keyPassword supplied by the referenced credential.

The fix correctly:

  1. Checks if keyPassword is already set by credential resolution (line 1245 in src/backend/database/routes/ssh.ts).
  2. Only uses host.key_password if keyPassword is undefined (line 1246).
  3. Always removes the snake_case version (key_password) to maintain a clean, camelCase API contract (line 1249).

Result: This prevents stale inline credentials from overwriting credential-resolved values and ensures proper precedence when building the API response.

# Key Password Precedence Fix This document summarizes the small but critical fix that prevents stale inline `key_password` values from overwriting credential-resolved `keyPassword` values when resolving SSH host credentials. ```typescript const result = { ...host }; if (host.key_password !== undefined) { // Only use the inline key_password if keyPassword hasn't been set by credential resolution if (result.keyPassword === undefined) { result.keyPassword = host.key_password; } // Always remove the snake_case version to standardize the output delete result.key_password; } ``` Why this matters - Hosts can be migrated from inline credentials to credential-backed configurations. If the old `key_password` remains on the host row, it must not overwrite the `keyPassword` supplied by the referenced credential. The fix correctly: 1. Checks if `keyPassword` is already set by credential resolution (line 1245 in `src/backend/database/routes/ssh.ts`). 2. Only uses `host.key_password` if `keyPassword` is `undefined` (line 1246). 3. Always removes the snake_case version (`key_password`) to maintain a clean, camelCase API contract (line 1249). Result: This prevents stale inline credentials from overwriting credential-resolved values and ensures proper precedence when building the API response.
Sign in to join this conversation.