anky2002 commited on
Commit
5e6ea60
·
verified ·
1 Parent(s): bf8b1ef

fix(security): Settings API uses real encryption, never returns raw keys

Browse files
Files changed (1) hide show
  1. backend/app/api/routes/settings.py +22 -29
backend/app/api/routes/settings.py CHANGED
@@ -1,24 +1,26 @@
1
  """
2
- Settings routes - API key management, user preferences.
3
  """
4
  import uuid
5
  from datetime import datetime
6
 
7
  from fastapi import APIRouter, HTTPException, status
8
- from pydantic import BaseModel
9
  from sqlalchemy import select
10
 
11
  from app.api.deps import CurrentUser, DBSession
 
12
  from app.models.user import User, UserAPIKey
13
 
14
  router = APIRouter(prefix="/settings", tags=["settings"])
15
 
16
 
17
- # ----- Schemas -----
 
18
 
19
  class APIKeyCreate(BaseModel):
20
  provider: str
21
- key: str
22
  label: str | None = None
23
 
24
 
@@ -26,6 +28,7 @@ class APIKeyResponse(BaseModel):
26
  id: str
27
  provider: str
28
  label: str | None
 
29
  is_active: bool
30
  created_at: datetime
31
 
@@ -40,14 +43,11 @@ class APIKeysListResponse(BaseModel):
40
  class UserPreferencesUpdate(BaseModel):
41
  default_provider: str | None = None
42
  default_model: str | None = None
43
- theme: str | None = None
44
-
45
 
46
- # ----- Routes -----
47
 
48
  @router.get("/api-keys", response_model=APIKeysListResponse)
49
  async def list_api_keys(db: DBSession, user: CurrentUser):
50
- """List user's API keys (without exposing the actual key values)."""
51
  result = await db.execute(
52
  select(UserAPIKey).where(UserAPIKey.user_id == user.id).order_by(UserAPIKey.created_at.desc())
53
  )
@@ -58,6 +58,7 @@ async def list_api_keys(db: DBSession, user: CurrentUser):
58
  id=str(k.id),
59
  provider=k.provider,
60
  label=k.label,
 
61
  is_active=k.is_active,
62
  created_at=k.created_at,
63
  )
@@ -68,35 +69,26 @@ async def list_api_keys(db: DBSession, user: CurrentUser):
68
 
69
  @router.post("/api-keys", response_model=APIKeyResponse, status_code=status.HTTP_201_CREATED)
70
  async def add_api_key(data: APIKeyCreate, db: DBSession, user: CurrentUser):
71
- """Add an API key for a provider."""
72
- # Validate provider
73
- valid_providers = ["openai", "anthropic", "google", "mistral", "deepseek", "xai", "huggingface"]
74
- if data.provider not in valid_providers:
75
- raise HTTPException(
76
- status_code=status.HTTP_400_BAD_REQUEST,
77
- detail=f"Invalid provider. Must be one of: {', '.join(valid_providers)}",
78
- )
79
 
80
- # Check if key already exists for this provider
81
  existing = await db.execute(
82
  select(UserAPIKey).where(
83
- UserAPIKey.user_id == user.id,
84
- UserAPIKey.provider == data.provider,
85
- UserAPIKey.is_active == True,
86
  )
87
  )
88
  if existing.scalar_one_or_none():
89
- raise HTTPException(
90
- status_code=status.HTTP_409_CONFLICT,
91
- detail=f"Active key already exists for {data.provider}. Delete it first.",
92
- )
93
 
94
- # In production: encrypt the key before storing
95
- # For now, store as-is (add encryption layer before deploying)
96
  key = UserAPIKey(
97
  user_id=user.id,
98
  provider=data.provider,
99
- encrypted_key=data.key, # TODO: encrypt with user-specific key
100
  label=data.label or f"{data.provider} key",
101
  is_active=True,
102
  )
@@ -108,6 +100,7 @@ async def add_api_key(data: APIKeyCreate, db: DBSession, user: CurrentUser):
108
  id=str(key.id),
109
  provider=key.provider,
110
  label=key.label,
 
111
  is_active=key.is_active,
112
  created_at=key.created_at,
113
  )
@@ -121,7 +114,7 @@ async def delete_api_key(key_id: uuid.UUID, db: DBSession, user: CurrentUser):
121
  )
122
  key = result.scalar_one_or_none()
123
  if not key:
124
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
125
  await db.delete(key)
126
 
127
 
@@ -132,4 +125,4 @@ async def update_preferences(data: UserPreferencesUpdate, db: DBSession, user: C
132
  update_data = data.model_dump(exclude_unset=True)
133
  prefs.update(update_data)
134
  user.llm_preferences = prefs
135
- return {"detail": "Preferences updated", "preferences": prefs}
 
1
  """
2
+ Settings routes - API key management with REAL encryption.
3
  """
4
  import uuid
5
  from datetime import datetime
6
 
7
  from fastapi import APIRouter, HTTPException, status
8
+ from pydantic import BaseModel, Field
9
  from sqlalchemy import select
10
 
11
  from app.api.deps import CurrentUser, DBSession
12
+ from app.core.encryption import encrypt_api_key, decrypt_api_key, mask_api_key
13
  from app.models.user import User, UserAPIKey
14
 
15
  router = APIRouter(prefix="/settings", tags=["settings"])
16
 
17
 
18
+ VALID_PROVIDERS = ["openai", "anthropic", "google", "mistral", "deepseek", "xai", "huggingface"]
19
+
20
 
21
  class APIKeyCreate(BaseModel):
22
  provider: str
23
+ key: str = Field(min_length=10, max_length=500)
24
  label: str | None = None
25
 
26
 
 
28
  id: str
29
  provider: str
30
  label: str | None
31
+ masked_key: str # Only shows sk-...last4
32
  is_active: bool
33
  created_at: datetime
34
 
 
43
  class UserPreferencesUpdate(BaseModel):
44
  default_provider: str | None = None
45
  default_model: str | None = None
 
 
46
 
 
47
 
48
  @router.get("/api-keys", response_model=APIKeysListResponse)
49
  async def list_api_keys(db: DBSession, user: CurrentUser):
50
+ """List user's API keys (NEVER exposes actual key values)."""
51
  result = await db.execute(
52
  select(UserAPIKey).where(UserAPIKey.user_id == user.id).order_by(UserAPIKey.created_at.desc())
53
  )
 
58
  id=str(k.id),
59
  provider=k.provider,
60
  label=k.label,
61
+ masked_key=mask_api_key(decrypt_api_key(k.encrypted_key) or "****"),
62
  is_active=k.is_active,
63
  created_at=k.created_at,
64
  )
 
69
 
70
  @router.post("/api-keys", response_model=APIKeyResponse, status_code=status.HTTP_201_CREATED)
71
  async def add_api_key(data: APIKeyCreate, db: DBSession, user: CurrentUser):
72
+ """Add an API key — encrypted at rest with Fernet."""
73
+ if data.provider not in VALID_PROVIDERS:
74
+ raise HTTPException(status_code=400, detail=f"Provider must be: {', '.join(VALID_PROVIDERS)}")
 
 
 
 
 
75
 
76
+ # Check existing
77
  existing = await db.execute(
78
  select(UserAPIKey).where(
79
+ UserAPIKey.user_id == user.id, UserAPIKey.provider == data.provider, UserAPIKey.is_active == True
 
 
80
  )
81
  )
82
  if existing.scalar_one_or_none():
83
+ raise HTTPException(status_code=409, detail=f"Active key exists for {data.provider}. Delete first.")
84
+
85
+ # Encrypt the key
86
+ encrypted = encrypt_api_key(data.key)
87
 
 
 
88
  key = UserAPIKey(
89
  user_id=user.id,
90
  provider=data.provider,
91
+ encrypted_key=encrypted,
92
  label=data.label or f"{data.provider} key",
93
  is_active=True,
94
  )
 
100
  id=str(key.id),
101
  provider=key.provider,
102
  label=key.label,
103
+ masked_key=mask_api_key(data.key),
104
  is_active=key.is_active,
105
  created_at=key.created_at,
106
  )
 
114
  )
115
  key = result.scalar_one_or_none()
116
  if not key:
117
+ raise HTTPException(status_code=404, detail="API key not found")
118
  await db.delete(key)
119
 
120
 
 
125
  update_data = data.model_dump(exclude_unset=True)
126
  prefs.update(update_data)
127
  user.llm_preferences = prefs
128
+ return {"detail": "Updated", "preferences": prefs}