diff --git a/scripts/setup-api.sh b/scripts/setup-api.sh index 9efea3e..0aea96f 100755 --- a/scripts/setup-api.sh +++ b/scripts/setup-api.sh @@ -1,18 +1,19 @@ #!/bin/bash # T3MP3ST API Key Setup Script +set -euo pipefail +umask 077 + echo "" echo "╔═══════════════════════════════════════════════════════════╗" echo "║ T3MP3ST API KEY CONFIGURATION ║" echo "╚═══════════════════════════════════════════════════════════╝" echo "" -# Check if .env exists ENV_FILE="$(dirname "$0")/../.env" if [ -f "$ENV_FILE" ]; then - echo "[*] Existing .env file found" - source "$ENV_FILE" + echo "[*] Existing .env file found; it will be replaced after you enter a new key." fi echo "" @@ -22,29 +23,38 @@ echo " 2) Anthropic (Claude direct)" echo " 3) OpenAI (GPT models)" echo "" -read -p "Enter choice [1-3]: " choice +read -r -p "Enter choice [1-3]: " choice case $choice in 1) echo "" echo "Get your OpenRouter API key at: https://openrouter.ai/keys" - read -p "Enter your OpenRouter API key: " api_key - echo "OPENROUTER_API_KEY=$api_key" > "$ENV_FILE" - echo "LLM_PROVIDER=openrouter" >> "$ENV_FILE" + read -rsp "Enter your OpenRouter API key: " api_key + echo "" + { + printf 'OPENROUTER_API_KEY=%s\n' "$api_key" + printf 'LLM_PROVIDER=openrouter\n' + } > "$ENV_FILE" ;; 2) echo "" echo "Get your Anthropic API key at: https://console.anthropic.com/" - read -p "Enter your Anthropic API key: " api_key - echo "ANTHROPIC_API_KEY=$api_key" > "$ENV_FILE" - echo "LLM_PROVIDER=anthropic" >> "$ENV_FILE" + read -rsp "Enter your Anthropic API key: " api_key + echo "" + { + printf 'ANTHROPIC_API_KEY=%s\n' "$api_key" + printf 'LLM_PROVIDER=anthropic\n' + } > "$ENV_FILE" ;; 3) echo "" echo "Get your OpenAI API key at: https://platform.openai.com/api-keys" - read -p "Enter your OpenAI API key: " api_key - echo "OPENAI_API_KEY=$api_key" > "$ENV_FILE" - echo "LLM_PROVIDER=openai" >> "$ENV_FILE" + read -rsp "Enter your OpenAI API key: " api_key + echo "" + { + printf 'OPENAI_API_KEY=%s\n' "$api_key" + printf 'LLM_PROVIDER=openai\n' + } > "$ENV_FILE" ;; *) echo "Invalid choice" @@ -52,6 +62,8 @@ case $choice in ;; esac +chmod 600 "$ENV_FILE" + # Add .env to gitignore if not already there GITIGNORE="$(dirname "$0")/../.gitignore" if [ -f "$GITIGNORE" ]; then @@ -69,7 +81,7 @@ echo "╔═══════════════════════ echo "║ CONFIGURATION COMPLETE! ║" echo "╚═══════════════════════════════════════════════════════════╝" echo "" -echo "Your API key has been saved to .env" +echo "Your API key has been saved to .env (mode 600)" echo "" echo "Start the server with:" echo " npm run server" diff --git a/src/__tests__/api-key-env-static.test.ts b/src/__tests__/api-key-env-static.test.ts new file mode 100644 index 0000000..5964acc --- /dev/null +++ b/src/__tests__/api-key-env-static.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +const configSource = readFileSync(join(process.cwd(), 'src/config/index.ts'), 'utf8'); +const setupScript = readFileSync(join(process.cwd(), 'scripts/setup-api.sh'), 'utf8'); + +function configLoadEnvBlock(): string { + const start = configSource.indexOf('private loadEnvVariables(): void'); + expect(start).toBeGreaterThanOrEqual(0); + const end = configSource.indexOf('\n /**\n * Get all settings', start); + expect(end).toBeGreaterThan(start); + return configSource.slice(start, end); +} + +describe('API key environment handling hardening', () => { + it('ConfigManager does not implicitly read .env from the caller working directory', () => { + const block = configLoadEnvBlock(); + + expect(block).not.toContain("join(process.cwd(), '.env')"); + expect(block).toContain("join(homedir(), '.t3mp3st', '.env')"); + }); + + it('ConfigManager uses env keys in-memory and does not persist imported env keys', () => { + const block = configLoadEnvBlock(); + + expect(block).not.toMatch(/setApiKey\(/); + expect(block).not.toMatch(/config\.set\(['"]apiKeys['"]/); + }); + + it('setup-api.sh never sources .env and reads API keys silently into a 0600 file', () => { + expect(setupScript).not.toMatch(/\bsource\s+["']?\$ENV_FILE/); + expect(setupScript).toMatch(/umask\s+077/); + expect(setupScript).toMatch(/read\s+-rsp\s+"Enter your OpenRouter API key:/); + expect(setupScript).toMatch(/chmod\s+600\s+"\$ENV_FILE"/); + }); +}); diff --git a/src/config/index.ts b/src/config/index.ts index e77053e..e498d74 100755 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -452,9 +452,10 @@ class ConfigManager { private loadEnvVariables(): void { if (this.envLoaded) return; - // Try to load from .env file in current directory or home + // Load only T3MP3ST-owned/home env files. Do NOT read process.cwd()/.env: + // operators often run T3MP3ST inside target repos, and importing that repo's + // secrets would contaminate this process with unrelated credentials. const envPaths = [ - join(process.cwd(), '.env'), join(homedir(), '.t3mp3st', '.env'), join(homedir(), '.env'), ]; @@ -482,23 +483,9 @@ class ConfigManager { } } - // Check environment variables for API keys - const envKeys = { - openrouter: process.env.OPENROUTER_API_KEY, - venice: process.env.VENICE_API_KEY, - anthropic: process.env.ANTHROPIC_API_KEY, - openai: process.env.OPENAI_API_KEY, - }; - - // Only set from env if not already set in config - const currentKeys = this.config.get('apiKeys'); - - for (const [provider, envKey] of Object.entries(envKeys)) { - if (envKey && !currentKeys[provider as keyof typeof currentKeys]) { - this.setApiKey(provider as 'openrouter' | 'venice' | 'anthropic' | 'openai', envKey); - } - } - + // Environment variables (including values loaded from ~/.t3mp3st/.env above) + // are intentionally process-local. getApiKey() already gives env vars highest + // priority, so do not persist them into Conf's apiKeys store as a side effect. this.envLoaded = true; }