Skip to content

Conversation

Copy link

Copilot AI commented Oct 21, 2025

Session-Scoped Public/Private Key Encryption for Remote Secrets (MVP)

This PR implements session-scoped encryption for remote secrets, allowing users to encrypt secrets client-side before sending them to the MCP server. This ensures plaintext secrets are never exposed to the LLM or persisted on the server.

Features

Session-Scoped Keypair Generation

  • A new X25519 public/private keypair is generated for each MCP session
  • Private key is stored in memory only and automatically destroyed on session end
  • Public key is exposed via MCP resource at mcp+session://encryption/pubkey
  • Each keypair has a unique kid (key ID) for validation

Client-Side Encryption with salt-sealed: URIs

Users can encrypt secrets on their own machine and use them with the salt-sealed: URI prefix:

  • Python: PyNaCl with libsodium sealed-box to create encrypted files
  • JavaScript: libsodium.js in the browser
  • salt-sealed: URI format: salt-sealed:/absolute/path/to/file.sealed
  • Salt-sealed files contain JSON with {"kid": "...", "ciphertext": "..."}
  • After decryption, content is parsed as dotenv format with dot notation support

Decrypt-On-Use Flow

  • Secrets are decrypted only when needed, in-process
  • Plaintext buffers are immediately zeroized after use
  • No plaintext ever persists on disk or in logs
  • 64 KB size limit enforced to prevent abuse

Security Model

# Step 1: Get public key (via MCP tool or resource)
get_encryption_instructions()

# Step 2: Create encrypted file with dotenv content
# (Python script creates a JSON file with kid and ciphertext)

# Step 3: Use salt-sealed URI
config = hydrate_config(
    base_config,
    dotenv_file_uris="salt-sealed:/path/to/secrets.sealed"
)

Implementation Details

Algorithm Choice

Selected libsodium sealed-box (X25519 + XSalsa20-Poly1305) for:

  • Simplicity and ease of use
  • Wide support across languages (Python, JavaScript, Rust, Go)
  • Copy-paste friendliness for MVP
  • Battle-tested cryptography library

Feature Flag

Encryption is disabled by default via the ENABLE_SESSION_ENCRYPTION environment variable:

export ENABLE_SESSION_ENCRYPTION=true
uvx airbyte-connector-builder-mcp

This ensures backward compatibility and allows users to opt-in when they need the extra security.

New Components

connector_builder_mcp/encryption.py (252 lines):

  • Core encryption module with keypair management
  • initialize_session_keypair(): Generate new keypair on startup
  • destroy_session_keypair(): Clean up on shutdown
  • decrypt_secret(): Decrypt with validation and error handling
  • get_public_key_info(): Retrieve public key details
  • encrypt_for_testing(): Test helper (not for production use)

docs/encryption.md (217 lines):

  • Complete user documentation
  • Setup and usage instructions
  • Client-side encryption examples (Python, JavaScript)
  • Security considerations and limitations
  • Troubleshooting guide

Modified Components

connector_builder_mcp/server.py:

  • Initialize keypair on server startup
  • Register public key as MCP resource
  • Destroy keypair on shutdown (in finally block)

connector_builder_mcp/secrets.py:

  • Added salt-sealed: URI support for encrypted secrets
  • Salt-sealed URIs work with the existing dotenv URI system
  • Added helper functions: _is_salt_sealed_url(), _fetch_salt_sealed_content()
  • Updated _validate_secrets_uris() to validate salt-sealed URIs
  • Updated _load_secrets() to handle salt-sealed URIs with dot notation parsing
  • Added get_encryption_instructions() tool for user guidance

Security Guarantees

No plaintext at rest: Secrets decrypted only when needed, immediately discarded
No logging: Secrets never appear in logs
Session-scoped: Keypairs destroyed on server shutdown
Buffer zeroization: Plaintext buffers cleared (to extent Python allows)
Size limits: 64 KB maximum ciphertext size
Kid validation: Rejects ciphertext from wrong session
Clear errors: Helpful messages for invalid ciphertext, kid mismatch, size violations
Absolute paths: Salt-sealed URIs require absolute paths for security

Testing

  • 22 encryption unit tests: Key generation, encryption/decryption, size limits, error handling
  • 4 salt-sealed integration tests: Salt-sealed URI hydration, combined sources, failures
  • 84 total tests passing: All existing tests continue to pass
  • CodeQL security scan: 0 vulnerabilities found
  • Linting: All ruff checks passing

Usage Example

# 1. Enable encryption (restart server)
# export ENABLE_SESSION_ENCRYPTION=true

# 2. Get encryption instructions
instructions = get_encryption_instructions()
# Returns public key, kid, algorithm details, and client-side encryption examples

# 3. Create encrypted secrets file (Python)
# Script creates /path/to/secrets.sealed with JSON: {"kid": "...", "ciphertext": "..."}
# The ciphertext contains dotenv-format content: api_key=value\ncredentials.password=secret

# 4. Use in connector config
config = hydrate_config(
    {"host": "api.example.com"},
    dotenv_file_uris="salt-sealed:/path/to/secrets.sealed"
)

# Can also combine with regular dotenv files
config = hydrate_config(
    base_config,
    dotenv_file_uris=[
        "/path/to/config.env",
        "salt-sealed:/path/to/secrets.sealed"
    ]
)

Non-Goals (Future Work)

This MVP focuses on the core encryption infrastructure. Future enhancements may include:

  • Hosting our own encryption webapp (Phase 2)
  • Durable secret storage with encryption at rest
  • Multi-session secret reuse
  • Alternative algorithms (JWE, age, etc.)

Dependencies

Added PyNaCl (v1.5.0+) for libsodium bindings:

  • Well-maintained and widely used
  • No known vulnerabilities (checked via GitHub Advisory Database)
  • Minimal additional dependencies

Documentation

  • docs/encryption.md: Complete user guide with salt-sealed examples
  • README.md: Updated to mention encryption feature
  • Inline code documentation and type hints throughout
  • Updated encryption instructions tool with salt-sealed usage

Recent Changes

  • Refactored get_encryption_instructions() to use constant strings defined at the top of secrets.py for better maintainability
  • Refactored to use salt-sealed: URI prefix: Replaced separate encrypted_secrets parameter with URI-based approach compatible with existing dotenv URI system

Fixes #147

Original prompt

This section details on the original issue you should resolve

<issue_title>Session-Scoped Public/Private Key Encryption for Remote Secrets (MVP; Third-Party Encryptor)</issue_title>
<issue_description>
Summary
Add first-phase support for remote secret intake without exposing plaintext to the LLM or persisting plaintext on the server.
On MCP session start, generate a per-session keypair, expose the public key as a resource, and accept client-side-encrypted ciphertext in tool calls.
For this MVP, users will create ciphertext via a trusted third-party site (or an optional one-liner CLI), then paste the ciphertext into the MCP tool call.


🎯 Goals / Acceptance Criteria

  • On session init, server generates a per-session keypair and stores the private key in memory only, destroyed when the session ends or times out.
  • Expose the public key as a resource (e.g. mcp+session://{id}/secret-pubkey), including kid, alg, and encoding details.
  • Add tool input support for { ciphertext, kid, ... } (e.g. connectors.create_*_ephemeral({ ciphertext, kid, ... })).
  • Implement a decrypt-on-use flow: decrypt, use the secret in-process, zeroize buffers; no plaintext at rest.
  • Provide step-by-step user instructions:
    1. Copy the public key.
    2. Visit a trusted client-side encryption site.
    3. Paste their secret and encrypt it.
    4. Paste the resulting ciphertext into the MCP tool call.
  • Add a size limit (≤ 64 KB) and clear validation errors for malformed ciphertexts.
  • Security considerations: no logging, zeroization of buffers, plane separation for connector execution.
  • Add a small placeholder for algorithm (see below).
  • Feature-flag this functionality behind an env var (off by default).

⚙️ Algorithm (TBD)

  • Option A: JWE (RSA-OAEP-256 + A256GCM) via WebCrypto-friendly format.
  • Option B: libsodium sealed-box (X25519 + XChaCha20/XSalsa20-Poly1305).
  • Implementor will select based on simplicity and copy-paste friendliness.

🧪 Tests

  • Unit tests for key generation, decrypt path, size limits, kid mismatch, and invalid ciphertext handling.

🧰 Docs (for MVP)

  • Where to find the public-key resource.
  • Recommended third-party sites for encryption (client-side only).
  • Optional one-liner CLI (uvx/npx) for power users.

🚫 Non-Goals

  • Hosting our own encryption webapp (that’s Phase 2).
  • Durable secret storage (plaintext or ciphertext persistence).
    </issue_description>

Comments on the Issue (you are @copilot in this section)

Fixes #147


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/add-session-scoped-key-encryption

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

👋 Welcome to the Airbyte Connector Builder MCP!

Thank you for your contribution! Here are some helpful tips and reminders for your convenience.

Testing This Branch via MCP

To test the changes in this specific branch with an MCP client like Claude Desktop, use the following configuration:

{
  "mcpServers": {
    "connector-builder-mcp-dev": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/airbytehq/connector-builder-mcp.git@copilot/add-session-scoped-key-encryption", "connector-builder-mcp"]
    }
  }
}

Testing This Branch via CLI

You can test this version of the MCP Server using the following CLI snippet:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/connector-builder-mcp.git@copilot/add-session-scoped-key-encryption#egg=airbyte-connector-builder-mcp' --help

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /build-connector - Builds the default connector on-demand using the AI builder
  • /build-connector prompt="<your prompt>" - Builds a connector on-demand using the AI builder
  • /poe <command> - Runs any poe command in the uv virtual environment

AI Builder Evaluations

AI builder evaluations run automatically under the following conditions:

  • When a PR is marked as "ready for review"
  • When a PR is reopened

A set of standardized evaluations also run on a schedule (Mon/Wed/Fri at midnight UTC) and can be manually triggered via workflow dispatch.

Helpful Resources

If you have any questions, feel free to ask in the PR comments or join our Slack community.

📝 Edit this welcome message.

@github-actions
Copy link

PyTest Results (Fast)

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit d979568. ± Comparison against base commit cbcdc20.

Copilot AI changed the title [WIP] Add session-scoped keypair encryption for remote secrets Add session-scoped public/private key encryption for remote secrets (MVP) Oct 21, 2025
Copilot AI requested a review from aaronsteers October 21, 2025 22:15
Copilot finished work on behalf of aaronsteers October 21, 2025 22:15
Copilot finished work on behalf of aaronsteers October 21, 2025 22:20
@aaronsteers
Copy link
Contributor

aaronsteers commented Oct 21, 2025

@copilot - Let's make the libsodium implementation compatible with the prior secret URIs as input args. Let's implement salt-sealed: as a prefix that triggers libsodium handling. Then, you can remove the new arg that is libsodium-specific, instead expecting the text to be a dotenv-styled cypertext, poitned to by salt-sealed:<cyphertext>

Copy link
Author

Copilot AI commented Oct 21, 2025

@copilot - Let's make the libsodium implementation compatible with the prior secret URIs as input args. Let's implement salt-sealed: as a prefix that triggers libsodium handling. Then, you can remov...

To clarify the implementation: should salt-sealed: work like this?

Option A - File-based:

salt-sealed:/path/to/encrypted.env

Where the file contains JSON: {"kid": "session-id", "ciphertext": "base64..."}
After decryption, the result is dotenv format content.

Option B - Inline:

salt-sealed:session-id:base64-ciphertext

Where the ciphertext when decrypted contains dotenv format content.

Which approach do you prefer? I'm leaning toward Option A as it's more consistent with how privatebin URLs work.

@copilot - Let's make the libsodium implementation compatible with the prior secret URIs as input args. Let's implement salt-sealed: as a prefix that triggers libsodium handling. Then, you can remov...

Implemented the salt-sealed: URI prefix approach. Changes in commit ae32cba:

Key changes:

  • Added salt-sealed: URI support that works with the existing dotenv URI system
  • Removed the encrypted_secrets parameter from hydrate_config()
  • Salt-sealed files contain JSON: {"kid": "session-id", "ciphertext": "base64..."}
  • After decryption, content is parsed as dotenv format with dot notation support
  • Consistent with privatebin URL pattern

Usage:

# Create encrypted file with dotenv content
# File: /path/to/secrets.sealed
# {"kid": "abc123", "ciphertext": "encrypted-dotenv-content"}

# Use with salt-sealed URI
config = hydrate_config(
    base_config,
    dotenv_file_uris="salt-sealed:/path/to/secrets.sealed"
)

# Can combine with regular dotenv files
config = hydrate_config(
    base_config,
    dotenv_file_uris=[
        "/path/to/config.env",
        "salt-sealed:/path/to/secrets.sealed"
    ]
)

The encrypted file's content (after decryption) should be in dotenv format with support for dot notation (e.g., credentials.password=secret).

…rameter

- Add salt-sealed: URI support for encrypted secrets files
- Remove encrypted_secrets parameter from hydrate_config()
- Salt-sealed files contain JSON with kid and ciphertext
- After decryption, content is parsed as dotenv format
- Update tests to use salt-sealed URI approach
- Update documentation with new usage examples

Co-authored-by: aaronsteers <[email protected]>
Copilot finished work on behalf of aaronsteers October 21, 2025 22:51
@aaronsteers aaronsteers changed the title Add session-scoped public/private key encryption for remote secrets (MVP) spike(do-not-merge): Add session-scoped public/private key encryption for remote secrets (MVP) Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session-Scoped Public/Private Key Encryption for Remote Secrets (MVP; Third-Party Encryptor)

2 participants