feat: standalone claude-memory-mcp with multi-user support and Vault integration
Extracted from private infra repo into standalone open-source project. Three operating modes: - Local: SQLite + FTS5 (zero dependencies) - Server: PostgreSQL via HTTP API with multi-user auth - Full: PostgreSQL + HashiCorp Vault for secret management Features: - MCP stdio server with 5 tools (store/recall/list/delete/secret_get) - FastAPI HTTP API with multi-user Bearer token auth (API_KEYS JSON map) - Regex-based credential detection with auto-redaction - AES-256-GCM encryption fallback for non-Vault deployments - Vault KV v2 client (stdlib urllib, K8s SA auto-auth) - Per-user data isolation (all queries scoped by user_id) - Secret migration endpoint for existing plain-text credentials - Backward-compatible env var aliases (CLAUDE_MEMORY_API_URL) Infrastructure: - Docker + docker-compose (API + PostgreSQL + optional Vault) - Woodpecker CI (test → build → push → kubectl deploy) - GitHub Actions CI (Python 3.11/3.12/3.13) + Release (GHCR + PyPI) - Helm chart + raw Kubernetes manifests 96 tests passing across 6 test files.
This commit is contained in:
commit
0ed5e1e016
40 changed files with 3381 additions and 0 deletions
71
src/claude_memory/crypto.py
Normal file
71
src/claude_memory/crypto.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""AES-256-GCM encryption for memory content when Vault is not available."""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
ENCRYPTION_KEY_ENV = "MEMORY_ENCRYPTION_KEY"
|
||||
|
||||
|
||||
def _get_key() -> bytes | None:
|
||||
"""Get 32-byte encryption key from environment."""
|
||||
raw = os.environ.get(ENCRYPTION_KEY_ENV)
|
||||
if not raw:
|
||||
return None
|
||||
# Accept hex-encoded 32-byte key or derive from passphrase
|
||||
try:
|
||||
key = bytes.fromhex(raw)
|
||||
if len(key) == 32:
|
||||
return key
|
||||
except ValueError:
|
||||
pass
|
||||
# Derive key from passphrase using SHA-256
|
||||
return hashlib.sha256(raw.encode()).digest()
|
||||
|
||||
|
||||
def is_encryption_configured() -> bool:
|
||||
return _get_key() is not None
|
||||
|
||||
|
||||
def encrypt(plaintext: str) -> bytes:
|
||||
"""Encrypt text using AES-256-GCM. Returns nonce + ciphertext + tag."""
|
||||
key = _get_key()
|
||||
if key is None:
|
||||
raise RuntimeError(f"{ENCRYPTION_KEY_ENV} not set")
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
except ImportError:
|
||||
raise RuntimeError("cryptography package required for encryption: pip install cryptography")
|
||||
|
||||
nonce = os.urandom(12)
|
||||
aesgcm = AESGCM(key)
|
||||
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
|
||||
return nonce + ciphertext # 12 bytes nonce + ciphertext + 16 bytes tag
|
||||
|
||||
|
||||
def decrypt(data: bytes) -> str:
|
||||
"""Decrypt AES-256-GCM encrypted data."""
|
||||
key = _get_key()
|
||||
if key is None:
|
||||
raise RuntimeError(f"{ENCRYPTION_KEY_ENV} not set")
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
except ImportError:
|
||||
raise RuntimeError("cryptography package required for encryption: pip install cryptography")
|
||||
|
||||
nonce = data[:12]
|
||||
ciphertext = data[12:]
|
||||
aesgcm = AESGCM(key)
|
||||
return aesgcm.decrypt(nonce, ciphertext, None).decode()
|
||||
|
||||
|
||||
def encrypt_b64(plaintext: str) -> str:
|
||||
"""Encrypt and return base64-encoded string."""
|
||||
return base64.b64encode(encrypt(plaintext)).decode()
|
||||
|
||||
|
||||
def decrypt_b64(data: str) -> str:
|
||||
"""Decrypt from base64-encoded string."""
|
||||
return decrypt(base64.b64decode(data))
|
||||
Loading…
Add table
Add a link
Reference in a new issue