Encrypt file paths in API URLs using Fernet tokens
Raw filesystem paths were exposed in browser URLs, dev tools, and proxy logs. Now all file-serving endpoints accept an opaque encrypted token (t= param) derived from the session secret via HKDF, with a 4-hour TTL. Backend: - Add core/path_tokens.py with Fernet encrypt/decrypt (HKDF from .session_secret) - Add file_token to all list/gallery/feed/search responses across 7 routers - Accept optional t= param on all file-serving endpoints (backward compatible) Frontend: - Update 4 URL helpers in api.ts to prefer token when available - Add 4 new helpers for paid-content/embedded-metadata URLs - Update all 14 page/component files to pass file_token to URL builders - Add file_token to all relevant TypeScript interfaces Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
57
web/backend/core/path_tokens.py
Normal file
57
web/backend/core/path_tokens.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Path Token Encryption
|
||||
|
||||
Encrypts file paths into opaque, time-limited Fernet tokens so raw filesystem
|
||||
paths are never exposed in API URLs.
|
||||
|
||||
encode_path("/opt/immich/paid/creator/file.mp4")
|
||||
# => "gAAAAABn..." (URL-safe, 4-hour TTL)
|
||||
|
||||
decode_path("gAAAAABn...")
|
||||
# => "/opt/immich/paid/creator/file.mp4"
|
||||
"""
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
def _get_fernet() -> Fernet:
|
||||
"""Derive a Fernet key from .session_secret via HKDF (cached after first call)."""
|
||||
if not hasattr(_get_fernet, "_instance"):
|
||||
from ..core.config import settings
|
||||
secret_path = settings.PROJECT_ROOT / ".session_secret"
|
||||
secret = secret_path.read_text().strip().encode()
|
||||
|
||||
derived = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=b"path-token-v1",
|
||||
info=b"fernet-key",
|
||||
).derive(secret)
|
||||
|
||||
key = base64.urlsafe_b64encode(derived)
|
||||
_get_fernet._instance = Fernet(key)
|
||||
return _get_fernet._instance
|
||||
|
||||
|
||||
def encode_path(file_path: str) -> str:
|
||||
"""Encrypt a file path into a URL-safe Fernet token (4-hour TTL)."""
|
||||
f = _get_fernet()
|
||||
return f.encrypt(file_path.encode()).decode()
|
||||
|
||||
|
||||
def decode_path(token: str) -> str:
|
||||
"""Decrypt a Fernet token back to a file path.
|
||||
|
||||
Raises HTTP 400 on invalid or expired tokens (TTL = 4 hours).
|
||||
"""
|
||||
f = _get_fernet()
|
||||
try:
|
||||
return f.decrypt(token.encode(), ttl=14400).decode()
|
||||
except InvalidToken:
|
||||
raise HTTPException(status_code=400, detail="Invalid or expired file token")
|
||||
Reference in New Issue
Block a user