diff --git a/cortex/api_key_detector.py b/cortex/api_key_detector.py index eebfd4a3..fb8535e5 100644 --- a/cortex/api_key_detector.py +++ b/cortex/api_key_detector.py @@ -7,21 +7,23 @@ Detection order (highest priority first): 1. CORTEX_PROVIDER=ollama environment variable (for explicit Ollama mode) 2. API key environment variables: ANTHROPIC_API_KEY, OPENAI_API_KEY -3. Cached key location (~/.cortex/.api_key_cache) -4. Saved Ollama provider preference in ~/.cortex/.env (CORTEX_PROVIDER=ollama) -5. API keys in ~/.cortex/.env -6. ~/.config/anthropic/credentials.json (Claude CLI location) -7. ~/.config/openai/credentials.json -8. .env in current directory +3. Encrypted storage (~/.cortex/environments/cortex.json) - secure Fernet-encrypted storage +4. Cached key location (~/.cortex/.api_key_cache) +5. Saved Ollama provider preference in ~/.cortex/.env (CORTEX_PROVIDER=ollama) +6. API keys in ~/.cortex/.env +7. ~/.config/anthropic/credentials.json (Claude CLI location) +8. ~/.config/openai/credentials.json +9. .env in current directory Implements caching to avoid repeated file checks, file locking for safe -concurrent access, and supports manual entry with optional saving to -~/.cortex/.env. +concurrent access, encrypted storage for sensitive keys, and supports +manual entry with optional saving. """ import fcntl import json +import logging import os import re from pathlib import Path @@ -29,6 +31,8 @@ from cortex.branding import console, cx_print +logger = logging.getLogger(__name__) + # Constants CORTEX_DIR = ".cortex" CORTEX_ENV_FILE = ".env" @@ -74,6 +78,14 @@ def detect(self) -> tuple[bool, str | None, str | None, str | None]: """ Auto-detect API key from common locations. + Detection order (highest priority first): + 1. CORTEX_PROVIDER=ollama environment variable + 2. API key environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY) + 3. Encrypted storage (~/.cortex/environments/cortex.json) + 4. Cached key location + 5. Saved Ollama provider preference + 6. File-based locations (.env files, credentials.json) + Returns: Tuple of (found, key, provider, source) - found: True if key was found @@ -90,6 +102,11 @@ def detect(self) -> tuple[bool, str | None, str | None, str | None]: if result: return result + # Check encrypted storage (secure storage from first-run wizard) + result = self._check_encrypted_storage() + if result: + return result + # Check cached location result = self._check_cached_key() if result: @@ -113,6 +130,34 @@ def _check_environment_api_keys(self) -> tuple[bool, str, str, str] | None: return (True, value, provider, "environment") return None + def _check_encrypted_storage(self) -> tuple[bool, str, str, str] | None: + """Check for API keys in encrypted storage (~/.cortex/environments/cortex.json). + + This is the secure storage location used by the first-run wizard. + Keys are stored encrypted using Fernet encryption. + """ + try: + from cortex.env_manager import get_env_manager + + env_mgr = get_env_manager() + + # Check for API keys in encrypted storage + for env_var, provider in ENV_VAR_PROVIDERS.items(): + value = env_mgr.get_variable(app="cortex", key=env_var, decrypt=True) + if value: + # Set in environment for this session + os.environ[env_var] = value + logger.debug(f"Loaded {env_var} from encrypted storage") + return (True, value, provider, "encrypted storage (~/.cortex/environments/)") + except ImportError: + # cryptography not installed, skip encrypted storage check + logger.debug("cryptography not installed, skipping encrypted storage check") + except Exception as e: + # Log but don't fail - other detection methods may work + logger.debug(f"Could not check encrypted storage: {e}") + + return None + def _check_saved_ollama_provider(self) -> tuple[bool, str, str, str] | None: """Check if Ollama was previously selected as the provider in config file.""" env_file = Path.home() / CORTEX_DIR / CORTEX_ENV_FILE diff --git a/cortex/first_run_wizard.py b/cortex/first_run_wizard.py index 6c7d22f9..bf8ad5ac 100644 --- a/cortex/first_run_wizard.py +++ b/cortex/first_run_wizard.py @@ -19,8 +19,13 @@ from pathlib import Path from typing import Any +from cortex.env_manager import get_env_manager + logger = logging.getLogger(__name__) +# Application name for storing cortex API keys +CORTEX_APP_NAME = "cortex" + class WizardStep(Enum): """Steps in the first-run wizard.""" @@ -786,21 +791,42 @@ def _prompt(self, message: str, default: str = "") -> str: return default def _save_env_var(self, name: str, value: str): - """Save environment variable to shell config.""" - shell = os.environ.get("SHELL", "/bin/bash") - shell_name = os.path.basename(shell) - config_file = self._get_shell_config(shell_name) + """Save environment variable securely using encrypted storage. - export_line = f'\nexport {name}="{value}"\n' # nosec - intentional user config storage + API keys are stored encrypted in ~/.cortex/environments/cortex.json + using Fernet encryption. The encryption key is stored in + ~/.cortex/.env_key with restricted permissions (chmod 600). + """ + # Set for current session regardless of storage success + os.environ[name] = value try: - with open(config_file, "a") as f: - f.write(export_line) - - # Also set for current session - os.environ[name] = value + env_mgr = get_env_manager() + + # Handle brand names correctly (e.g., "OpenAI" not "Openai") + provider_name_raw = name.replace("_API_KEY", "") + if provider_name_raw == "OPENAI": + provider_name_display = "OpenAI" + elif provider_name_raw == "ANTHROPIC": + provider_name_display = "Anthropic" + else: + provider_name_display = provider_name_raw.replace("_", " ").title() + + env_mgr.set_variable( + app=CORTEX_APP_NAME, + key=name, + value=value, + encrypt=True, + description=f"API key for {provider_name_display}", + ) + logger.info(f"Saved {name} to encrypted storage") + except ImportError: + logger.warning( + f"cryptography package not installed. {name} set for current session only. " + "Install cryptography for persistent encrypted storage: pip install cryptography" + ) except Exception as e: - logger.warning(f"Could not save env var: {e}") + logger.warning(f"Could not save env var to encrypted storage: {e}") # Convenience functions