File size: 2,331 Bytes
3f2c936
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""登录: 标注人账号在环境变量 ANNOTATORS_JSON 里配置(密码只存 PBKDF2 哈希), 会话用 HMAC 签名 cookie。

ANNOTATORS_JSON 示例(scripts/hash_password.py 生成哈希):
{
  "Rong": {"role": "admin", "pw": "pbkdf2_sha256$..."},
  "A":    {"pw": "pbkdf2_sha256$...", "papers": ["MICCAI_3979_paper"]},
  "B":    {"pw": "pbkdf2_sha256$..."}
}
papers 可选: 给了就只能看到这些论文。repo 可选: 给了就把此人标注写进那个仓库(默认写 DATA_REPO 的 annotations/<ID>/)。未配置 ANNOTATORS_JSON 且为 local 后端时 = 开放模式(同 0817 本地版, 任填 ID)。
"""
import base64
import hashlib
import hmac
import json
import secrets
import time
from typing import Optional

ITERATIONS = 200_000
COOKIE = "repl_sess"
SESSION_TTL = 14 * 24 * 3600


def hash_password(pw: str, salt: Optional[str] = None) -> str:
    salt = salt or secrets.token_hex(16)
    dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), ITERATIONS)
    return f"pbkdf2_sha256${ITERATIONS}${salt}${dk.hex()}"


def verify_password(pw: str, stored: str) -> bool:
    try:
        algo, it, salt, digest = stored.split("$")
    except ValueError:
        return False
    if algo != "pbkdf2_sha256":
        return False
    dk = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), int(it))
    return hmac.compare_digest(dk.hex(), digest)


def _b64(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).decode().rstrip("=")


def _unb64(s: str) -> bytes:
    return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))


def sign_session(secret: str, annotator: str) -> str:
    body = _b64(json.dumps({"a": annotator, "exp": int(time.time()) + SESSION_TTL}).encode())
    mac = _b64(hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest())
    return f"{body}.{mac}"


def read_session(secret: str, token: Optional[str]) -> Optional[str]:
    if not token or "." not in token:
        return None
    body, mac = token.rsplit(".", 1)
    want = _b64(hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest())
    if not hmac.compare_digest(mac, want):
        return None
    try:
        data = json.loads(_unb64(body))
    except (ValueError, json.JSONDecodeError):
        return None
    if data.get("exp", 0) < time.time():
        return None
    return data.get("a")