91719740b9
- Неверный пароль не сжигает wrap сразу: Argon2-гейт до consume и лимит попыток (по умолчанию 3) в настройках - Подсветка синтаксиса, автоопределение языка, спиннеры, размеры файлов, пагинация audit - make push (git, Ctrl-D) и актуальный README
74 lines
2.3 KiB
Python
74 lines
2.3 KiB
Python
from __future__ import annotations
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models import (
|
|
DEFAULT_MIME_ALLOWLIST,
|
|
AppSettings,
|
|
CaptchaProvider,
|
|
PasswordMode,
|
|
)
|
|
|
|
|
|
PASSWORD_MODE_HELP = {
|
|
"client_only": (
|
|
"Password also wraps ciphertext in the browser (PBKDF2 + AES-GCM). "
|
|
"Server still stores an Argon2id hash and rejects wrong passwords before "
|
|
"the one-time unwrap, so a mistyped password does not burn the package."
|
|
),
|
|
"server_gate": (
|
|
"Server stores an Argon2id hash and releases ciphertext only after a correct password. "
|
|
"Ciphertext is still encrypted client-side with the same password. "
|
|
"Stops offline bruteforce if someone steals the share link without the password."
|
|
),
|
|
}
|
|
|
|
|
|
async def get_or_create_settings(db: AsyncSession) -> AppSettings:
|
|
result = await db.execute(select(AppSettings).where(AppSettings.id == 1))
|
|
row = result.scalar_one_or_none()
|
|
if row:
|
|
return row
|
|
row = AppSettings(
|
|
id=1,
|
|
mime_allowlist=list(DEFAULT_MIME_ALLOWLIST),
|
|
captcha_provider=CaptchaProvider.off,
|
|
password_mode=PasswordMode.client_only,
|
|
)
|
|
db.add(row)
|
|
await db.commit()
|
|
await db.refresh(row)
|
|
return row
|
|
|
|
|
|
def mime_allowed(allowlist: list[str], content_type: str) -> bool:
|
|
ct = (content_type or "").split(";")[0].strip().lower()
|
|
if not ct:
|
|
return False
|
|
for pattern in allowlist:
|
|
p = pattern.strip().lower()
|
|
if not p:
|
|
continue
|
|
if p.endswith("/*"):
|
|
if ct.startswith(p[:-1]):
|
|
return True
|
|
elif ct == p:
|
|
return True
|
|
return False
|
|
|
|
|
|
def public_settings_payload(row: AppSettings) -> dict:
|
|
return {
|
|
"max_upload_bytes": row.max_upload_bytes,
|
|
"default_ttl_seconds": row.default_ttl_seconds,
|
|
"max_ttl_seconds": row.max_ttl_seconds,
|
|
"mime_allowlist": list(row.mime_allowlist or []),
|
|
"captcha_provider": row.captcha_provider.value,
|
|
"turnstile_site_key": row.turnstile_site_key or "",
|
|
"hcaptcha_site_key": row.hcaptcha_site_key or "",
|
|
"password_mode": row.password_mode.value,
|
|
"password_mode_description": PASSWORD_MODE_HELP,
|
|
"password_max_attempts": max(1, int(row.password_max_attempts or 3)),
|
|
}
|