sachin commited on
Commit
56859f5
·
1 Parent(s): c8002a8

add-encrity

Browse files
Files changed (2) hide show
  1. src/server/main.py +40 -23
  2. src/server/utils/auth.py +58 -23
src/server/main.py CHANGED
@@ -5,7 +5,7 @@ from typing import List, Optional
5
  from abc import ABC, abstractmethod
6
 
7
  import uvicorn
8
- from fastapi import Depends, FastAPI, File, HTTPException, Query, Request, UploadFile, Form
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse
11
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
@@ -14,6 +14,8 @@ from slowapi import Limiter
14
  from slowapi.util import get_remote_address
15
  import requests
16
  from PIL import Image
 
 
17
 
18
  # Import from auth.py
19
  from utils.auth import get_current_user, get_current_user_with_admin, login, refresh_token, register, app_register, TokenResponse, Settings, LoginRequest, RegisterRequest, bearer_scheme
@@ -58,7 +60,7 @@ async def get_user_id_for_rate_limit(request: Request):
58
  user_id = await get_current_user(credentials)
59
  return user_id
60
  except Exception:
61
- return get_remote_address(request) # Fallback to IP if unauthenticated
62
 
63
  limiter = Limiter(key_func=get_user_id_for_rate_limit)
64
 
@@ -155,14 +157,18 @@ async def home():
155
  @app.post("/v1/token",
156
  response_model=TokenResponse,
157
  summary="User Login",
158
- description="Authenticate a user with username and password to obtain an access token and refresh token. Copy the access token and use it in the 'Authorize' button above.",
159
  tags=["Authentication"],
160
  responses={
161
  200: {"description": "Successful login", "model": TokenResponse},
162
- 401: {"description": "Invalid username or password"}
 
163
  })
164
- async def token(login_request: LoginRequest):
165
- return await login(login_request)
 
 
 
166
 
167
  @app.post("/v1/refresh",
168
  response_model=TokenResponse,
@@ -195,20 +201,21 @@ async def register_user(
195
  @app.post("/v1/app/register",
196
  response_model=TokenResponse,
197
  summary="Register New App User",
198
- description="Create a new user account for the mobile app using an email and device token. Returns an access token and refresh token. Rate limited to 5 requests per minute per IP.",
199
  tags=["Authentication"],
200
  responses={
201
  200: {"description": "User registered successfully", "model": TokenResponse},
202
- 400: {"description": "Email already registered"},
203
  429: {"description": "Rate limit exceeded"}
204
  })
205
  @limiter.limit(settings.speech_rate_limit)
206
  async def app_register_user(
207
  request: Request,
208
- register_request: RegisterRequest
 
209
  ):
210
- logger.info(f"App registration attempt for email: {register_request.username}")
211
- return await app_register(register_request)
212
 
213
  @app.post("/v1/audio/speech",
214
  summary="Generate Speech from Text",
@@ -551,8 +558,8 @@ async def translate(
551
 
552
  class VisualQueryRequest(BaseModel):
553
  query: str
554
- src_lang: str = "kan_Knda" # Default to Kannada
555
- tgt_lang: str = "kan_Knda" # Default to Kannada
556
 
557
  @field_validator("query")
558
  def query_must_be_valid(cls, v):
@@ -633,25 +640,31 @@ async def visual_query(
633
  except ValueError as e:
634
  logger.error(f"Invalid JSON response: {str(e)}")
635
  raise HTTPException(status_code=500, detail="Invalid response format from visual query service")
636
-
637
- # Ensure these imports are at the top with other imports
638
- from fastapi.responses import StreamingResponse
639
  from enum import Enum
640
 
641
- # Define supported languages for validation
642
  class SupportedLanguage(str, Enum):
643
  kannada = "kannada"
644
  hindi = "hindi"
645
  tamil = "tamil"
646
 
647
- # Add the speech-to-speech endpoint
 
 
 
 
 
 
 
 
 
648
  @app.post("/v1/speech_to_speech",
649
  summary="Speech-to-Speech Conversion",
650
- description="Convert input speech to processed speech by calling an external speech-to-speech API. Rate limited to 5 requests per minute per user. Requires authentication.",
651
  tags=["Audio"],
652
  responses={
653
  200: {"description": "Audio stream", "content": {"audio/mp3": {"example": "Binary audio data"}}},
654
- 400: {"description": "Invalid input"},
655
  401: {"description": "Unauthorized - Token required"},
656
  429: {"description": "Rate limit exceeded"},
657
  504: {"description": "External API timeout"},
@@ -660,11 +673,14 @@ class SupportedLanguage(str, Enum):
660
  @limiter.limit(settings.speech_rate_limit)
661
  async def speech_to_speech(
662
  request: Request,
663
- file: UploadFile = File(..., description="Audio file to process"),
664
  language: SupportedLanguage = Query(..., description="Language of the audio (kannada, hindi, tamil)"),
665
- credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)
 
666
  ) -> StreamingResponse:
667
  user_id = await get_current_user(credentials)
 
 
668
  logger.info("Processing speech-to-speech request", extra={
669
  "endpoint": "/v1/speech_to_speech",
670
  "audio_filename": file.filename,
@@ -674,7 +690,8 @@ async def speech_to_speech(
674
  })
675
 
676
  try:
677
- file_content = await file.read()
 
678
  files = {"file": (file.filename, file_content, file.content_type)}
679
  external_url = f"https://slabstech-dhwani-internal-api-server.hf.space/v1/speech_to_speech?language={language}"
680
 
 
5
  from abc import ABC, abstractmethod
6
 
7
  import uvicorn
8
+ from fastapi import Depends, FastAPI, File, HTTPException, Query, Request, UploadFile, Form, Header
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse
11
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 
14
  from slowapi.util import get_remote_address
15
  import requests
16
  from PIL import Image
17
+ import base64
18
+ from Crypto.Cipher import AES
19
 
20
  # Import from auth.py
21
  from utils.auth import get_current_user, get_current_user_with_admin, login, refresh_token, register, app_register, TokenResponse, Settings, LoginRequest, RegisterRequest, bearer_scheme
 
60
  user_id = await get_current_user(credentials)
61
  return user_id
62
  except Exception:
63
+ return get_remote_address(request)
64
 
65
  limiter = Limiter(key_func=get_user_id_for_rate_limit)
66
 
 
157
  @app.post("/v1/token",
158
  response_model=TokenResponse,
159
  summary="User Login",
160
+ description="Authenticate a user with encrypted email and device token to obtain an access token and refresh token. Requires X-Session-Key header.",
161
  tags=["Authentication"],
162
  responses={
163
  200: {"description": "Successful login", "model": TokenResponse},
164
+ 400: {"description": "Invalid encrypted data"},
165
+ 401: {"description": "Invalid email or device token"}
166
  })
167
+ async def token(
168
+ login_request: LoginRequest,
169
+ x_session_key: str = Header(..., alias="X-Session-Key")
170
+ ):
171
+ return await login(login_request, x_session_key)
172
 
173
  @app.post("/v1/refresh",
174
  response_model=TokenResponse,
 
201
  @app.post("/v1/app/register",
202
  response_model=TokenResponse,
203
  summary="Register New App User",
204
+ description="Create a new user account for the mobile app using an encrypted email and device token. Returns an access token and refresh token. Rate limited to 5 requests per minute per IP. Requires X-Session-Key header.",
205
  tags=["Authentication"],
206
  responses={
207
  200: {"description": "User registered successfully", "model": TokenResponse},
208
+ 400: {"description": "Email already registered or invalid encrypted data"},
209
  429: {"description": "Rate limit exceeded"}
210
  })
211
  @limiter.limit(settings.speech_rate_limit)
212
  async def app_register_user(
213
  request: Request,
214
+ register_request: RegisterRequest,
215
+ x_session_key: str = Header(..., alias="X-Session-Key")
216
  ):
217
+ logger.info(f"App registration attempt")
218
+ return await app_register(register_request, x_session_key)
219
 
220
  @app.post("/v1/audio/speech",
221
  summary="Generate Speech from Text",
 
558
 
559
  class VisualQueryRequest(BaseModel):
560
  query: str
561
+ src_lang: str = "kan_Knda"
562
+ tgt_lang: str = "kan_Knda"
563
 
564
  @field_validator("query")
565
  def query_must_be_valid(cls, v):
 
640
  except ValueError as e:
641
  logger.error(f"Invalid JSON response: {str(e)}")
642
  raise HTTPException(status_code=500, detail="Invalid response format from visual query service")
643
+
 
 
644
  from enum import Enum
645
 
 
646
  class SupportedLanguage(str, Enum):
647
  kannada = "kannada"
648
  hindi = "hindi"
649
  tamil = "tamil"
650
 
651
+ def decrypt_audio(encrypted_data: bytes, key: bytes) -> bytes:
652
+ try:
653
+ nonce, ciphertext = encrypted_data[:12], encrypted_data[12:]
654
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
655
+ plaintext = cipher.decrypt_and_verify(ciphertext[:-16], ciphertext[-16:])
656
+ return plaintext
657
+ except Exception as e:
658
+ logger.error(f"Audio decryption failed: {str(e)}")
659
+ raise HTTPException(status_code=400, detail="Invalid encrypted audio")
660
+
661
  @app.post("/v1/speech_to_speech",
662
  summary="Speech-to-Speech Conversion",
663
+ description="Convert input speech to processed speech by calling an external speech-to-speech API. Rate limited to 5 requests per minute per user. Requires authentication and X-Session-Key header.",
664
  tags=["Audio"],
665
  responses={
666
  200: {"description": "Audio stream", "content": {"audio/mp3": {"example": "Binary audio data"}}},
667
+ 400: {"description": "Invalid input or encrypted audio"},
668
  401: {"description": "Unauthorized - Token required"},
669
  429: {"description": "Rate limit exceeded"},
670
  504: {"description": "External API timeout"},
 
673
  @limiter.limit(settings.speech_rate_limit)
674
  async def speech_to_speech(
675
  request: Request,
676
+ file: UploadFile = File(..., description="Encrypted audio file to process"),
677
  language: SupportedLanguage = Query(..., description="Language of the audio (kannada, hindi, tamil)"),
678
+ credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
679
+ x_session_key: str = Header(..., alias="X-Session-Key")
680
  ) -> StreamingResponse:
681
  user_id = await get_current_user(credentials)
682
+ session_key = base64.b64decode(x_session_key)
683
+
684
  logger.info("Processing speech-to-speech request", extra={
685
  "endpoint": "/v1/speech_to_speech",
686
  "audio_filename": file.filename,
 
690
  })
691
 
692
  try:
693
+ encrypted_content = await file.read()
694
+ file_content = decrypt_audio(encrypted_content, session_key)
695
  files = {"file": (file.filename, file_content, file.content_type)}
696
  external_url = f"https://slabstech-dhwani-internal-api-server.hf.space/v1/speech_to_speech?language={language}"
697
 
src/server/utils/auth.py CHANGED
@@ -10,6 +10,9 @@ from sqlalchemy.ext.declarative import declarative_base
10
  from sqlalchemy.orm import sessionmaker
11
  from passlib.context import CryptContext
12
  import os
 
 
 
13
 
14
  # SQLite database setup with Hugging Face persistent storage
15
  DATABASE_PATH = "/data/users.db"
@@ -22,7 +25,8 @@ class User(Base):
22
  __tablename__ = "users"
23
  username = Column(String, primary_key=True, index=True)
24
  password = Column(String) # Stores hashed passwords
25
- is_admin = Column(Boolean, default=False) # New admin flag
 
26
 
27
  # Ensure the /data directory exists
28
  os.makedirs(os.path.dirname(DATABASE_PATH), exist_ok=True)
@@ -35,8 +39,8 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
35
 
36
  class Settings(BaseSettings):
37
  api_key_secret: str = Field(..., env="API_KEY_SECRET")
38
- token_expiration_minutes: int = Field(1440, env="TOKEN_EXPIRATION_MINUTES") # 24 hours
39
- refresh_token_expiration_days: int = Field(7, env="REFRESH_TOKEN_EXPIRATION_DAYS") # 7 days
40
  llm_model_name: str = "google/gemma-3-4b-it"
41
  max_tokens: int = 512
42
  host: str = "0.0.0.0"
@@ -61,19 +65,19 @@ settings = Settings()
61
  def seed_initial_data():
62
  db = SessionLocal()
63
  try:
64
- # Seed test user (non-admin) with a device token-like password
65
  test_username = "[email protected]"
66
  if not db.query(User).filter_by(username=test_username).first():
67
- test_device_token = "550e8400-e29b-41d4-a716-446655440000" # Sample UUID
68
  hashed_password = pwd_context.hash(test_device_token)
69
- db.add(User(username=test_username, password=hashed_password, is_admin=False))
 
70
  db.commit()
71
- # Seed admin user using environment variables
72
  admin_username = settings.default_admin_username
73
  admin_password = settings.default_admin_password
74
  if not db.query(User).filter_by(username=admin_username).first():
75
  hashed_password = pwd_context.hash(admin_password)
76
- db.add(User(username=admin_username, password=hashed_password, is_admin=True))
 
77
  db.commit()
78
  logger.info(f"Seeded initial data: test user '{test_username}', admin user '{admin_username}'")
79
  except Exception as e:
@@ -82,10 +86,8 @@ def seed_initial_data():
82
  finally:
83
  db.close()
84
 
85
- # Initialize database with seed data
86
  seed_initial_data()
87
 
88
- # Use HTTPBearer
89
  bearer_scheme = HTTPBearer()
90
 
91
  class TokenPayload(BaseModel):
@@ -106,6 +108,17 @@ class RegisterRequest(BaseModel):
106
  username: str
107
  password: str
108
 
 
 
 
 
 
 
 
 
 
 
 
109
  async def create_access_token(user_id: str) -> dict:
110
  expire = datetime.utcnow() + timedelta(minutes=settings.token_expiration_minutes)
111
  payload = {"sub": user_id, "exp": expire.timestamp(), "type": "access"}
@@ -166,14 +179,28 @@ async def get_current_user_with_admin(credentials: HTTPAuthorizationCredentials
166
  raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
167
  return user_id
168
 
169
- async def login(login_request: LoginRequest) -> TokenResponse:
170
  db = SessionLocal()
171
- user = db.query(User).filter_by(username=login_request.username).first()
172
- db.close()
173
- if not user or not pwd_context.verify(login_request.password, user.password):
174
- logger.warning(f"Login failed for user: {login_request.username}")
 
 
 
 
 
 
 
 
175
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or device token")
176
- tokens = await create_access_token(user_id=user.username)
 
 
 
 
 
 
177
  return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
178
 
179
  async def register(register_request: RegisterRequest, current_user: str = Depends(get_current_user_with_admin)) -> TokenResponse:
@@ -194,22 +221,30 @@ async def register(register_request: RegisterRequest, current_user: str = Depend
194
  logger.info(f"Registered and generated token for user: {register_request.username} by admin {current_user}")
195
  return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
196
 
197
- async def app_register(register_request: RegisterRequest) -> TokenResponse:
198
  db = SessionLocal()
199
- existing_user = db.query(User).filter_by(username=register_request.username).first()
 
 
 
 
 
 
 
 
200
  if existing_user:
201
  db.close()
202
- logger.warning(f"App registration failed: Email {register_request.username} already exists")
203
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
204
 
205
- hashed_password = pwd_context.hash(register_request.password)
206
- new_user = User(username=register_request.username, password=hashed_password, is_admin=False)
207
  db.add(new_user)
208
  db.commit()
209
  db.close()
210
 
211
- tokens = await create_access_token(user_id=register_request.username)
212
- logger.info(f"App registered new user: {register_request.username}")
213
  return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
214
 
215
  async def refresh_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> TokenResponse:
 
10
  from sqlalchemy.orm import sessionmaker
11
  from passlib.context import CryptContext
12
  import os
13
+ import base64
14
+ from Crypto.Cipher import AES
15
+ from Crypto.Random import get_random_bytes
16
 
17
  # SQLite database setup with Hugging Face persistent storage
18
  DATABASE_PATH = "/data/users.db"
 
25
  __tablename__ = "users"
26
  username = Column(String, primary_key=True, index=True)
27
  password = Column(String) # Stores hashed passwords
28
+ is_admin = Column(Boolean, default=False)
29
+ session_key = Column(String, nullable=True) # Stores base64-encoded session key
30
 
31
  # Ensure the /data directory exists
32
  os.makedirs(os.path.dirname(DATABASE_PATH), exist_ok=True)
 
39
 
40
  class Settings(BaseSettings):
41
  api_key_secret: str = Field(..., env="API_KEY_SECRET")
42
+ token_expiration_minutes: int = Field(1440, env="TOKEN_EXPIRATION_MINUTES")
43
+ refresh_token_expiration_days: int = Field(7, env="REFRESH_TOKEN_EXPIRATION_DAYS")
44
  llm_model_name: str = "google/gemma-3-4b-it"
45
  max_tokens: int = 512
46
  host: str = "0.0.0.0"
 
65
  def seed_initial_data():
66
  db = SessionLocal()
67
  try:
 
68
  test_username = "[email protected]"
69
  if not db.query(User).filter_by(username=test_username).first():
70
+ test_device_token = "550e8400-e29b-41d4-a716-446655440000"
71
  hashed_password = pwd_context.hash(test_device_token)
72
+ session_key = base64.b64encode(get_random_bytes(16)).decode('utf-8')
73
+ db.add(User(username=test_username, password=hashed_password, is_admin=False, session_key=session_key))
74
  db.commit()
 
75
  admin_username = settings.default_admin_username
76
  admin_password = settings.default_admin_password
77
  if not db.query(User).filter_by(username=admin_username).first():
78
  hashed_password = pwd_context.hash(admin_password)
79
+ session_key = base64.b64encode(get_random_bytes(16)).decode('utf-8')
80
+ db.add(User(username=admin_username, password=hashed_password, is_admin=True, session_key=session_key))
81
  db.commit()
82
  logger.info(f"Seeded initial data: test user '{test_username}', admin user '{admin_username}'")
83
  except Exception as e:
 
86
  finally:
87
  db.close()
88
 
 
89
  seed_initial_data()
90
 
 
91
  bearer_scheme = HTTPBearer()
92
 
93
  class TokenPayload(BaseModel):
 
108
  username: str
109
  password: str
110
 
111
+ def decrypt_data(encrypted_data: str, key: bytes) -> str:
112
+ try:
113
+ data = base64.b64decode(encrypted_data)
114
+ nonce, ciphertext = data[:12], data[12:]
115
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
116
+ plaintext = cipher.decrypt_and_verify(ciphertext[:-16], ciphertext[-16:])
117
+ return plaintext.decode('utf-8')
118
+ except Exception as e:
119
+ logger.error(f"Decryption failed: {str(e)}")
120
+ raise HTTPException(status_code=400, detail="Invalid encrypted data")
121
+
122
  async def create_access_token(user_id: str) -> dict:
123
  expire = datetime.utcnow() + timedelta(minutes=settings.token_expiration_minutes)
124
  payload = {"sub": user_id, "exp": expire.timestamp(), "type": "access"}
 
179
  raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
180
  return user_id
181
 
182
+ async def login(login_request: LoginRequest, session_key_b64: str) -> TokenResponse:
183
  db = SessionLocal()
184
+ session_key = base64.b64decode(session_key_b64)
185
+ try:
186
+ username = decrypt_data(login_request.username, session_key)
187
+ password = decrypt_data(login_request.password, session_key)
188
+ except:
189
+ db.close()
190
+ raise HTTPException(status_code=400, detail="Invalid encrypted data")
191
+
192
+ user = db.query(User).filter_by(username=username).first()
193
+ if not user or not pwd_context.verify(password, user.password):
194
+ db.close()
195
+ logger.warning(f"Login failed for user: {username}")
196
  raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or device token")
197
+
198
+ if user.session_key != session_key_b64:
199
+ user.session_key = session_key_b64
200
+ db.commit()
201
+ db.close()
202
+
203
+ tokens = await create_access_token(user_id=username)
204
  return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
205
 
206
  async def register(register_request: RegisterRequest, current_user: str = Depends(get_current_user_with_admin)) -> TokenResponse:
 
221
  logger.info(f"Registered and generated token for user: {register_request.username} by admin {current_user}")
222
  return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
223
 
224
+ async def app_register(register_request: RegisterRequest, session_key_b64: str) -> TokenResponse:
225
  db = SessionLocal()
226
+ session_key = base64.b64decode(session_key_b64)
227
+ try:
228
+ username = decrypt_data(register_request.username, session_key)
229
+ password = decrypt_data(register_request.password, session_key)
230
+ except:
231
+ db.close()
232
+ raise HTTPException(status_code=400, detail="Invalid encrypted data")
233
+
234
+ existing_user = db.query(User).filter_by(username=username).first()
235
  if existing_user:
236
  db.close()
237
+ logger.warning(f"App registration failed: Email {username} already exists")
238
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
239
 
240
+ hashed_password = pwd_context.hash(password)
241
+ new_user = User(username=username, password=hashed_password, is_admin=False, session_key=session_key_b64)
242
  db.add(new_user)
243
  db.commit()
244
  db.close()
245
 
246
+ tokens = await create_access_token(user_id=username)
247
+ logger.info(f"App registered new user: {username}")
248
  return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
249
 
250
  async def refresh_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> TokenResponse: