Skip to content

feat(ui-react): add Secure Vault for encrypted SSH private key storage#5903

Merged
gustavosbarreto merged 14 commits intomasterfrom
feat/ui-react/secure-vault
Mar 4, 2026
Merged

feat(ui-react): add Secure Vault for encrypted SSH private key storage#5903
gustavosbarreto merged 14 commits intomasterfrom
feat/ui-react/secure-vault

Conversation

@luizhf42
Copy link
Member

@luizhf42 luizhf42 commented Feb 26, 2026

What

Encrypted SSH private key storage in the browser, protected by a master password. Keys are encrypted with AES-256-GCM (PBKDF2-SHA256 derived key) and persisted in localStorage. Private key authentication uses SSH challenge-response signing — the raw key never leaves the browser.

Why

ShellHub previously stored private keys in plain text in localStorage. The Secure Vault encrypts keys at rest behind a master password, matching the security model of tools like Termius Vault. This is the community/self-hosted implementation; the backend adapter pattern (IVaultBackend) supports future server-side storage for cloud/enterprise.

Changes

  • Crypto layer (vault-crypto.ts): AES-256-GCM encryption/decryption, PBKDF2-SHA256 key derivation (600k iterations), verifier-based password checking without storing the password hash. Session key held in module scope (not in React state or localStorage).
  • Storage abstraction (vault-backend.ts, vault-backend-local.ts): IVaultBackend interface with localStorage implementation and singleton factory.
  • SSH key utilities (ssh-keys.ts): sshpk-based key validation with KeyEncryptedError detection for auto-detecting passphrase-protected keys, MD5 fingerprint extraction, algorithm/size detection (getAlgorithm), and challenge-response signature generation (RSA via node-rsa pkcs1-sha1; ED25519 via sshpk raw 64-byte output; ECDSA via sshpk with per-curve hash — SHA-256/nistp256, SHA-384/nistp384, SHA-512/nistp521 — and SSH wire format blob extraction).
  • Vault store (vaultStore.ts): Zustand store managing vault lifecycle (uninitialized/locked/unlocked), key CRUD with duplicate name/data prevention, master password change, vault reset, and legacy key migration from the Vue UI format.
  • Vault UI components: Setup dialog, unlock dialog (with autocomplete suppression), settings section (change password, lock, reset with typed confirmation).
  • Secure Vault page: Three-state page — onboarding landing for uninitialized, full-screen locked state (matching the FeatureGate pattern), and key management table. The table is always visible when unlocked, with an in-table empty state, search by name or fingerprint, add/edit drawer (auto-detects encrypted keys and prompts for passphrase), and delete confirmation. Each key row displays its algorithm (e.g. Ed25519, RSA 4096, ECDSA P-256) and a lock icon for passphrase-protected keys.
  • Terminal integration: TerminalInstance now implements the WebSocket challenge-response protocol (kind 3 Signature) for private key auth — POST sends fingerprint only, browser signs challenges locally. Sensitive key material is cleared immediately after signing and on component unmount. ConnectDrawer adds vault key selector with vault/manual toggle and unlock prompt when vault is locked.
  • Auth integration: Vault session key cleared on logout (explicit, token expiry, 401).
  • Reusable key file input: Extracted shared file input logic (drag-and-drop, paste interception, file/text mode toggle) into useKeyFileInput hook and KeyFileInput component, used by both public keys and secure vault drawers.
  • Dependencies: Added sshpk, node-rsa, vite-plugin-node-polyfills (provides Node.js globals for sshpk/node-rsa in browser).

Testing

  • Set up a vault, add keys (with and without passphrase), verify encrypted storage in localStorage (shellhub-vault-meta, shellhub-vault-data — no plaintext).
  • Connect to a device with private key auth via vault selector and manual input — verify challenge-response completes (no kind 4 error).
  • Test RSA, ED25519, and ECDSA key types for both vault storage and terminal connection.
  • Change master password, then verify old password fails and new password unlocks with all keys intact.
  • Reset vault, verify return to onboarding state.
  • Logout and verify vault locks (session key cleared).
  • If privateKeys exists in localStorage (legacy Vue UI), verify migration on first vault setup.
  • 322 unit/component tests covering store, crypto utils, SSH key utils, backend, hook, and UI components.

@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch 6 times, most recently from 06e4ad4 to ab6b7e7 Compare March 2, 2026 14:22
@luizhf42 luizhf42 marked this pull request as ready for review March 2, 2026 14:29
@luizhf42 luizhf42 requested a review from a team as a code owner March 2, 2026 14:29
@luizhf42 luizhf42 self-assigned this Mar 2, 2026
@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch from ab6b7e7 to 78bbe9f Compare March 2, 2026 14:31
@gustavosbarreto
Copy link
Member

/review

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Claude finished @gustavosbarreto's task in 8m 48s —— View job


Code Review Complete

Reviewed 39 files across code quality, security, testing, TypeScript/React patterns, and architecture. 14 inline issues posted (1 high, 9 medium, 4 low).

Highlights

Security — The crypto implementation is sound: AES-256-GCM with PBKDF2-SHA256 at 600k iterations, fresh random IVs, session key in module scope (not React state or localStorage), vault locked on logout. Challenge-response SSH signing correctly keeps the private key client-side. One gap: clearSensitiveData is missing in the signature failure path (see comment).

Architecture — The IVaultBackend interface is synchronous, which will require a breaking refactor when adding the cloud/enterprise server-side backend (see comment). Vault storage keys aren't namespaced per user (see comment).

Performance — Six components use useVaultStore() without selectors, causing unnecessary re-renders cascading from the page root (see comment).

Additional notes

Testing gaps — 311 tests cover the store, crypto, SSH utils, backend, hook, and some UI components well. However, four vault UI components have zero test coverage: VaultSetupDialog, VaultUnlockDialog, VaultSettingsSection (including ChangePasswordDrawer), and ConnectDrawer. These are security-sensitive components handling master password entry, vault initialization, password changes, and SSH key authentication. Also, TerminalInstance's new challenge-response signing flow has no tests.

Token expiry — The logout action correctly locks the vault, but when the session expires via API interceptor redirect (without a full page reload), the vault session key may remain in memory. The interceptor should also call useVaultStore.getState().lock().


If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch from 78bbe9f to 30ee484 Compare March 2, 2026 17:44
@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch 2 times, most recently from 9c89a8b to 807c09a Compare March 4, 2026 13:29
@gustavosbarreto
Copy link
Member

/review

@github-actions
Copy link

github-actions bot commented Mar 4, 2026

Claude finished @gustavosbarreto's task in 6m 31s —— View job


Code Review Complete

Reviewed 39 files across code quality, security, testing, TypeScript/React patterns, and architecture — no issues found.

All fixes from the previous five review rounds were verified as correctly applied. The code looks good as-is.


If you push additional changes and want a new review, tag @shellhub-io/admin and a team member can trigger it.

@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch 3 times, most recently from 8e3b35a to df7b68c Compare March 4, 2026 17:47
luizhf42 added 4 commits March 4, 2026 15:15
Add sshpk and node-rsa for SSH private key parsing, fingerprint
extraction, and challenge-response signing. Add
vite-plugin-node-polyfills to provide Node.js globals (process, Buffer)
required by these libraries in the browser environment.
Define VaultMeta, VaultData, VaultKeyEntry, VaultStatus, and
LegacyPrivateKey types used across the Secure Vault feature.
Implement AES-256-GCM encryption/decryption and PBKDF2-SHA256 key
derivation using the Web Crypto API. Includes session key management
(module-scoped CryptoKey), vault metadata creation, and password
verification via encrypted verifier pattern.
…tation

Define IVaultBackend interface for storage abstraction, implement
LocalVaultBackend using localStorage for community/self-hosted edition,
and add singleton factory for backend instantiation. The adapter pattern
allows future server-side storage without changing vault logic.
@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch 2 times, most recently from dc1b810 to cbfb45e Compare March 4, 2026 18:25
luizhf42 added 10 commits March 4, 2026 16:23
Add private key validation with encrypted key detection
(KeyEncryptedError), MD5 fingerprint extraction, and challenge-response
signature generation using sshpk and node-rsa. Supports RSA (pkcs1-sha1)
and ED25519/ECDSA (sha512) key types.
Implement vault state management with full lifecycle: initialize, unlock,
lock, and key CRUD operations (add, update, remove). Includes duplicate
key prevention (name and data uniqueness), master password change, vault
reset, and legacy key migration from the old Vue UI format.
Add VaultSetupDialog for first-time master password creation,
VaultUnlockDialog for vault unlock with autocomplete suppression,
VaultLockedBanner for locked state indication, and VaultSettingsSection
with master password change, lock, and reset vault functionality.
Add the main Secure Vault page with three states: uninitialized
(onboarding), locked (banner), and unlocked (key table with search).
Includes KeyDrawer for add/edit with automatic encrypted key detection
and passphrase validation, KeyDeleteDialog for key removal, and
duplicate key prevention with per-field error messages.
Clear the vault session key from memory when the user logs out,
covering explicit logout, token expiration, and 401 responses.
Extract the duplicated file-input logic (FileReader, 512KB limit, drag-and-drop,
paste interception, file/text mode toggle) into useKeyFileInput hook and
KeyFileInput component. Refactor public-keys KeyDataInput to use them.
Replace the inline file/text input block with the shared KeyFileInput
component, removing duplicated file-input logic.
Cover vault crypto utilities, localStorage backend, SSH key
utils, vault Zustand store, KeyFileInput hook and component,
and secure-vault page components.
@luizhf42 luizhf42 force-pushed the feat/ui-react/secure-vault branch from cbfb45e to c90cf52 Compare March 4, 2026 19:43
@gustavosbarreto
Copy link
Member

/review

@github-actions
Copy link

github-actions bot commented Mar 4, 2026

Claude finished @gustavosbarreto's task in 4m 18s —— View job


Code Review Complete

Reviewed 39 files across code quality, security, testing, TypeScript/React patterns, and architecture — no issues found.

All fixes from the previous five review rounds were verified as correctly applied. The code looks good as-is.


If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

View job

@gustavosbarreto gustavosbarreto merged commit 0432602 into master Mar 4, 2026
21 checks passed
@gustavosbarreto gustavosbarreto deleted the feat/ui-react/secure-vault branch March 4, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants