Metadata-Version: 2.4
Name: golike-gauth
Version: 0.1.5
Summary: Generate valid g-auth / g-device-id headers for Golike gateway API
Author: deno4908
License-Expression: MIT
Project-URL: Homepage, https://github.com/deno4908/golike-gauth
Project-URL: Repository, https://github.com/deno4908/golike-gauth
Project-URL: Issues, https://github.com/deno4908/golike-gauth/issues
Project-URL: Bug Tracker, https://github.com/deno4908/golike-gauth/issues
Keywords: golike,g-auth,gauth,aes-gcm,hkdf
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=41
Provides-Extra: requests
Requires-Dist: requests>=2.28; extra == "requests"
Provides-Extra: dev
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Requires-Dist: requests>=2.28; extra == "dev"
Dynamic: license-file

# golike-gauth

[![PyPI](https://img.shields.io/pypi/v/golike-gauth.svg)](https://pypi.org/project/golike-gauth/)
[![Python](https://img.shields.io/pypi/pyversions/golike-gauth.svg)](https://pypi.org/project/golike-gauth/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Generate valid **`g-auth`** / **`g-device-id`** (và **`sig`** TikTok) headers cho Golike gateway API.

**Repo:** https://github.com/deno4908/golike-gauth  
**Version hiện tại (source):** `0.1.5` — PyPI có thể còn `0.1.3` nếu chưa upload.

## Changelog

### 0.1.5 (mới — chưa / sắp lên PyPI)

- **Gỡ helper theo platform** (`get_instagram_job`, `get_tiktok_job`, `complete_*`, `skip_*`…).
- **Auth dùng chung:** chỉ còn `from_token` + `get` / `post` / `put` / `delete` / `request` / `headers`.
- Gọi API mọi platform theo path, ví dụ:
  ```python
  auth.get("/advertising/publishers/tiktok/jobs", params={"account_id": "1", "data": "null"})
  auth.post("/advertising/publishers/instagram/skip-jobs", json={...})
  ```
- CLI gọn: bootstrap bằng token, tuỳ chọn `--enable-sig` / `--no-sig`.

### 0.1.4

- Thêm **`enable_sig`** trên `GolikeAuth` / `from_token`:
  - `None` = auto (chỉ path TikTok jobs/complete)
  - `True` = bật `sig`
  - `False` = tắt `sig`
- Có thể ghi đè từng request: `auth.get(path, with_sig=True)`.

### 0.1.3

- Header **`sig`** cho TikTok (`generate_sig`, auto gắn khi path khớp).
- `build_headers` / `request` gộp query vào path khi ký `sig`.
- Helper TikTok (đã **gỡ ở 0.1.5** — dùng `get`/`post` chung).

### 0.1.2

- **`GolikeAuth.from_token(token)`** — chỉ cần JWT:
  - `user_id` từ JWT `sub`
  - `username` / profile từ `GET /users/me` (User-Agent **mobile**)
  - `signing_key` mặc định = `data.firebase_id`
  - `device_id` tự sinh UUID
- Hằng `MOBILE_UA`, `fetch_user_me`, `jwt_user_id`.
- `verify=True/False` khi bootstrap (echo check).

### 0.1.1 / 0.1.0

- Core: HKDF + AES-GCM `g-auth`, `g-device-id`, header `t`.
- `GolikeAuth` request ký body compact (không space).
- Fix `g-version` / `g-client` đúng app web.

### Nâng cấp từ 0.1.3

```bash
pip install -U golike-gauth
# hoặc local:
pip install -e ".[requests]"
```

**Breaking (0.1.5):** nếu code cũ gọi `auth.get_tiktok_job(...)` / `get_instagram_job(...)` → đổi sang `auth.get(...)` / `auth.post(...)` như trên.

## Install

```bash
pip install golike-gauth

# optional: HTTP helper (requests)
pip install "golike-gauth[requests]"

# ban moi nhat tu GitHub (neu PyPI chua cap nhat)
pip install -U git+https://github.com/deno4908/golike-gauth.git
```

## Quick start

### Chỉ cần token (khuyến dùng)

Hầu hết trường hợp **chỉ nhập JWT**. Các tham số còn lại lấy từ API (User-Agent **mobile**).

```python
from golike_gauth import GolikeAuth

auth = GolikeAuth.from_token("eyJ...")  # chỉ token

print(auth.user_id)       # JWT sub
print(auth.username)      # /users/me
print(auth.device_id)     # UUID tự sinh
print(auth.signing_key)   # = data.firebase_id (app web cũng set như vậy)
print(auth.profile)       # raw /users/me
```

#### Flow `from_token` làm gì?

| Bước | Nguồn | Kết quả |
|---|---|---|
| 1 | Decode JWT | `user_id` = `sub` |
| 2 | `GET /users/me` (UA mobile Android) | `username`, `coin`, `firebase_id`, … |
| 3 | `signing_key` | mặc định = **`data.firebase_id`** (trùng `store.state.signing_key` trên app) |
| 4 | `device_id` | UUID v4 (tự tạo, nên lưu lại) |
| 5 | (tuỳ chọn) `verify=True` | `POST /security/echo` kiểm tra key decrypt được |

```python
# verify echo (mac dinh True) — fail neu key sai
auth = GolikeAuth.from_token("eyJ...", verify=True)

# bo qua verify echo
auth = GolikeAuth.from_token("eyJ...", verify=False)

# hiếm khi firebase_id != store key → truyền tay:
auth = GolikeAuth.from_token(
    "eyJ...",
    signing_key="cxbbf6td1EXc...",  # store.state.signing_key
)
```

#### Lấy token từ đâu?

Trên https://app.golike.net (đã login, F12 → Network):

- Request bất kỳ → header `Authorization: Bearer eyJ...`
- Hoặc Application / Local Storage / vuex (field `token`)

`signing_key` thường **không cần** dán: app gán `store.signing_key = me.firebase_id`.

#### Ví dụ: lấy job Facebook chỉ với token

```python
from golike_gauth import GolikeAuth

auth = GolikeAuth.from_token("eyJ...", verify=False)
# dam bao dung firebase_id
if auth.profile.get("firebase_id"):
    auth.signing_key = auth.profile["firebase_id"]

# list account FB tren Golike
accs = auth.get("/fb-account?limit=200").json().get("data") or []
fb_id = accs[0]["fb_id"]

r = auth.get(
    f"/advertising/publishers/get-jobs-2026"
    f"?fb_id={fb_id}&server=sv2&high_job=1&low_job=1"
)
print(r.status_code, r.json())
```

Script mẫu trong workspace: `test_fb_jobs.py` (chỉ hỏi token).

### Thủ công (5 trường)

```python
from golike_gauth import GolikeAuth

auth = GolikeAuth(
    token="eyJ...",              # JWT Bearer
    signing_key="...",           # = firebase_id hoac store.state.signing_key
    user_id=123456,              # JWT sub
    username="your_username",
    device_id="32484704-8a4e-4909-9d42-866773b321d6",  # nên giữ cố định
)
```

Lib dùng được với **mọi** path gateway. Chỉ cần đúng **method + path + body/query** như browser.

### Cách dùng chung (khuyến nghị)

```python
# GET — không body, params = query string
r = auth.get("/path/to/api", params={"key": "value"})
print(r.status_code, r.json())

# POST — có JSON body (g-auth ký đúng body compact, không space)
r = auth.post("/path/to/api", json={"a": 1, "b": "x"})
print(r.status_code, r.json())

# method bất kỳ
r = auth.request("PUT", "/path/to/api", json={...})
r = auth.request("DELETE", "/path/to/api")
```

### Ví dụ thật theo platform

#### Instagram — lấy job (GET)

```python
r = auth.get(
    "/advertising/publishers/instagram/jobs",
    params={
        "instagram_account_id": "966624",
        "data": "null",
    },
)
# hoặc helper:
r = auth.get_instagram_job("966624")
print(r.json())
```

#### Instagram — skip job (POST, không phải GET!)

```python
r = auth.post(
    "/advertising/publishers/instagram/skip-jobs",
    json={
        "ads_id": 620978,
        "object_id": "6155111723",
        "account_id": 966624,
        "type": "follow",  # follow | like | comment | ...
    },
)
# hoặc helper:
r = auth.skip_instagram_job(
    ads_id=620978,
    object_id="6155111723",
    account_id=966624,
    type="follow",
)
print(r.json())
```

#### Instagram — complete job (POST)

```python
r = auth.post(
    "/advertising/publishers/instagram/complete-jobs",
    json={
        "instagram_users_advertising_id": 620978,
        "instagram_account_id": 966624,
        "async": True,
        "data": None,
    },
)
# hoặc helper:
r = auth.complete_instagram_job(
    instagram_users_advertising_id=620978,
    instagram_account_id=966624,
)
print(r.json())
```

#### Twitter / X — lấy job (GET)

Tương đương curl:

`GET /api/advertising/publishers/twitter/jobs?account_id=97445`

```python
r = auth.get(
    "/advertising/publishers/twitter/jobs",
    params={"account_id": "97445"},
)
print(r.status_code, r.json())
```

#### TikTok / Facebook / … (cùng pattern)

```python
# GET jobs (query tùy platform — copy từ Network tab browser)
r = auth.get(
    "/advertising/publishers/tiktok/jobs",
    params={"account_id": "123"},  # hoặc param khác tùy API
)

# POST skip / complete — luôn dùng auth.post(..., json={...})
r = auth.post(
    "/advertising/publishers/tiktok/skip-jobs",
    json={...},  # body copy từ Network tab
)
```

#### Users / endpoint khác

```python
r = auth.get("/users/me")
r = auth.post("/some/path", json={"foo": "bar"})
```

### Sai thường gặp

```python
# ❌ SAI: auth.post() đã gọi API, không phải headers
# ❌ SAI: skip-jobs dùng GET → 405
response = requests.get(
    "https://gateway.golike.net/api/advertising/publishers/instagram/skip-jobs",
    headers=auth.post("/advertising/publishers/instagram/skip-jobs", json={...}),
)

# ✅ ĐÚNG
response = auth.post(
    "/advertising/publishers/instagram/skip-jobs",
    json={
        "ads_id": 620978,
        "object_id": "6155111723",
        "account_id": 966624,
        "type": "follow",
    },
)
print(response.json())
```

Nếu tự dùng `requests`, **method + body bytes** phải khớp lúc ký:

```python
import json
import requests

body = {
    "ads_id": 620978,
    "object_id": "6155111723",
    "account_id": 966624,
    "type": "follow",
}
raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False)  # không space
headers = auth.headers(
    "POST",
    "/advertising/publishers/instagram/skip-jobs",
    body=raw,
)
response = requests.post(
    "https://gateway.golike.net/api/advertising/publishers/instagram/skip-jobs",
    headers=headers,
    data=raw.encode("utf-8"),
)
```

### Headers only / low-level

```python
from golike_gauth import generate_g_auth, generate_device_id, decode_g_auth

# headers: method/path/body phải trùng request thật
headers = auth.headers(
    "GET",
    "/advertising/publishers/twitter/jobs",
    body="",  # GET không body
)

g_auth = generate_g_auth(
    method="GET",
    path="/advertising/publishers/twitter/jobs",
    body="",
    signing_key="...",
    device_id=generate_device_id(),
    user_id=123456,
)
print(decode_g_auth(g_auth, "..."))
```

### Method cheatsheet

| API | Method | Params / Body |
|---|---|---|
| `/advertising/publishers/instagram/jobs` | **GET** | query: `instagram_account_id`, `data` |
| `/advertising/publishers/instagram/skip-jobs` | **POST** | JSON: `{ads_id, object_id, account_id, type}` |
| `/advertising/publishers/instagram/complete-jobs` | **POST** | JSON: `{instagram_users_advertising_id, instagram_account_id, async, data, ...}` |
| `/advertising/publishers/twitter/jobs` | **GET** | query: `account_id` |
| `/users/me` | **GET** | — |
| path khác | copy từ browser Network | **đúng method + body như browser** |

Quy tắc:

1. **GET** → `auth.get(path, params=...)` — body ký = `""`
2. **POST** → `auth.post(path, json=...)` — body ký = JSON compact
3. **405** = sai method (vd. GET `skip-jobs`)
4. **g-auth** tạo mới **mỗi request**, khớp method + path + body
5. Path truyền vào **không** cần `/api` prefix (`auth` tự thêm base `.../api`)

## Where to get `signing_key`

On https://app.golike.net (logged in), DevTools console:

```js
document.querySelector('#app').__vue__.$store.state.signing_key
```

> This value may differ from `data.firebase_id` in `/users/me`. Always use the store key the browser actually signs with.

## CLI

```bash
golike-gauth \
  --token eyJ... \
  --signing-key ... \
  --user-id 123456 \
  --username your_username \
  --call --ig-account-id YOUR_IG_ACCOUNT_ID
```

## Notes

- `g-auth` must be **regenerated for every request** (binds method + path + body hash + timestamp).
- GET requests sign body as empty string `""`.
- Path signed is pathname only, e.g. `/api/advertising/publishers/instagram/jobs`.

## Development

```bash
git clone https://github.com/deno4908/golike-gauth.git
cd golike-gauth
python -m pip install -e ".[dev,requests]"
```

### Build & publish (PyPI)

```bash
python -m pip install -U build twine
python -m build
python -m twine upload dist/*
```

## Contributing

Issues and PRs: https://github.com/deno4908/golike-gauth/issues

## License

[MIT](LICENSE) — see [LICENSE](https://github.com/deno4908/golike-gauth/blob/main/LICENSE)

---

# Phân tích mã hóa / giải mã `g-auth` (Golike Gateway)

Tài liệu này mô tả cách app web Golike tạo header **`g-auth`** cho mỗi request tới `gateway.golike.net`, và cách server (hoặc client debug) giải mã token đó.

Nguồn reverse: bundle `index-68ef440b.js` (các hàm `H_`, `q_`, `Mg`, `M_`, `j_`, `Jd`, `aA`, `bu`).

---

## 1. Vai trò của `g-auth`

`g-auth` là **request-binding token**: mỗi request HTTP mang một token mới, gắn với:

| Trường payload | Ý nghĩa |
|---|---|
| `n` | HTTP method (`GET` / `POST` / …) |
| `k` | Pathname API (không có query) |
| `q` | SHA-256 hex của body |
| `d` | `g-device-id` (UUID) |
| `u` | `user_id` |
| `t` | timestamp client (ms) |
| `x` | nonce ngẫu nhiên |
| `r` | digest ngắn chống giả mạo thô |

Token được **mã hóa AES-GCM** bằng khóa dẫn xuất từ `signing_key` của user. Server giải mã → kiểm tra method/path/body/device/user/time → cho phép request.

> `g-auth` **không tái sử dụng** được giữa các request (path/body/time khác nhau, và có nonce).

---

## 2. Các header liên quan

Ngoài JWT `Authorization: Bearer …`, client còn gửi:

| Header | Nguồn / công thức |
|---|---|
| `g-auth` | AES-GCM token (mục 4–5) |
| `g-device-id` | UUID v4, lưu `localStorage.device_id` |
| `g-username` | username user |
| `g-version` | version app, ví dụ `26.07.10.2` |
| `g-client` | client id app |
| `t` | `btoa(btoa(btoa(unix_seconds)))` — timestamp 3 lớp Base64 |

---

## 3. `signing_key` — nguyên liệu khóa

### 3.1. Lấy key ở đâu?

Trong Vuex store:

```js
$store.state.signing_key
// DevTools:
document.querySelector('#app').__vue__.$store.state.signing_key
```

> **Lưu ý thực tế:** giá trị store **có thể khác** `data.firebase_id` trả về từ `GET /users/me`.  
> Luôn dùng đúng key mà browser đang ký (store / console), không chỉ dựa vào field API.

Key là **32 bytes**, thường encode **Base64** (có `+`, `/`, `=`), ví dụ:

```text
cxbbf6td1EXcoEWlnk0eVmJwG1NJYhiqPxNcUXG+cBc=
```

Client cũng chấp nhận hex 64 ký tự.

### 3.2. Parse key (`k_`)

```
nếu chuỗi là hex hợp lệ và decode ra 32 bytes  → dùng
else decode Base64 / Base64URL → 32 bytes     → dùng
else lỗi: signing key must decode to 32 bytes
```

---

## 4. Dẫn xuất khóa AES (HKDF)

Trước khi AES-GCM, raw key 32 bytes được đưa qua **HKDF-SHA256**:

| Tham số | Giá trị |
|---|---|
| IKM | 32 bytes từ `signing_key` |
| Hash | SHA-256 |
| Salt | `glk-gauth-v3-2026q3` |
| Info | `aes-gcm-key` |
| Output length | 32 bytes |

```
AES_KEY = HKDF-SHA256(
  ikm  = decode(signing_key),
  salt = "glk-gauth-v3-2026q3",
  info = "aes-gcm-key",
  len  = 32
)
```

Hằng số trong JS:

```js
B_ = "glk-gauth"
E_ = "v3-2026"          // sn(247)
S_ = "q3"
Sg = B_ + "-" + E_ + S_ // => "glk-gauth-v3-2026q3"
I_ = "aes-gcm-key"
```

`AES_KEY` import vào WebCrypto / OpenSSL dưới dạng **AES-256-GCM**.

---

## 5. Tạo payload trước khi mã hóa

### 5.1. Chuẩn hóa path (`Pg` + `yu`)

- Bỏ query (`?...`) và hash (`#...`)
- Ghép với base `https://gateway.golike.net/api`
- Chỉ lấy **pathname**

Ví dụ:

```text
request URL:
  /advertising/publishers/instagram/jobs?instagram_account_id=966624&data=null

signed path k:
  /api/advertising/publishers/instagram/jobs
```

### 5.2. Hash body (`j_`)

```text
body_str =
  null/undefined  →  ""
  string          →  chính nó
  object          →  JSON.stringify(obj)   // compact, không space
                     // JS: JSON.stringify → {"a":1}

q = SHA256_hex(body_str)
```

**GET / HEAD / DELETE** trong app: body ký = chuỗi rỗng `""`

```text
q = SHA256("")
  = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```

### 5.3. Digest `r` (`M_`)

```text
r = SHA256_hex( f"{t}:{device_id}:{q}:{salt}" )[0:16]
```

với `salt = glk-gauth-v3-2026q3`, `t` là timestamp **milliseconds**.

### 5.4. Object payload đầy đủ

Thứ tự key (quan trọng vì `JSON.stringify` giữ insertion order):

```json
{
  "t": 1783944428398,
  "x": "z7Yv5E5ClYSc0gyyjllQ2w",
  "d": "32484704-8a4e-4909-9d42-866773b321d6",
  "u": 639111,
  "n": "GET",
  "k": "/api/advertising/publishers/instagram/jobs",
  "q": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "r": "286c4f5bd68ec1c4"
}
```

| Field | Mô tả |
|---|---|
| `t` | `Date.now()` (ms) |
| `x` | 16 bytes random → Base64URL (no padding) |
| `d` | device UUID (`g-device-id`) |
| `u` | user id (number) |
| `n` | method upper-case |
| `k` | pathname đã chuẩn hóa |
| `q` | body hash |
| `r` | digest 16 hex chars |

Chuỗi plaintext:

```text
plaintext = JSON.stringify(payload)   // separators mặc định JS: không space
         → UTF-8 bytes
```

---

## 6. Mã hóa (encrypt) — `H_`

```
IV  = 12 bytes random          (crypto.getRandomValues)
CT  = AES-256-GCM.Encrypt(
        key = AES_KEY,
        iv  = IV,
        pt  = plaintext_utf8
      )
      // CT = ciphertext || 16-byte auth tag  (WebCrypto / cryptography)

token_bytes = IV || CT
g-auth      = Base64URL(token_bytes)   // bỏ padding '='
```

Sơ đồ:

```
signing_key (b64/hex)
        │
        ▼ decode 32B
       IKM
        │
        ▼ HKDF-SHA256(salt, info)
     AES_KEY (32B)
        │
        │   ┌─ method, path, body, device, user, now ─┐
        │   ▼                                         │
        │  JSON payload {t,x,d,u,n,k,q,r}             │
        │   ▼ UTF-8                                   │
        │  plaintext ──────────┐                      │
        │                      │                      │
        ▼                      ▼                      │
   AES-256-GCM(IV=12B)  ←── encrypt                   │
        │                                             │
        ▼                                             │
   IV || ciphertext+tag                               │
        │                                             │
        ▼ Base64URL                                   │
     header g-auth ───────────────────────────────────┘
```

---

## 7. Giải mã (decrypt) — `q_` / server / debug

```
raw = Base64URL_decode(g-auth)
IV  = raw[0:12]
CT  = raw[12:]          // ciphertext + tag

plaintext = AES-256-GCM.Decrypt(AES_KEY, IV, CT)
payload   = JSON.parse(plaintext)
```

Sau khi giải mã, server thường kiểm tra:

1. **Decrypt OK** (key + IV + tag đúng)
   - Fail → `decrypt_fail_check_signing_key_or_iv_or_tag_or_r_field`
2. **`n`** khớp HTTP method
3. **`k`** khớp pathname canonical (vd. `/api/...`)
4. **`q`** khớp SHA256 body thực tế nhận được
5. **`d` / `u`** khớp session / JWT
6. **`t`** không lệch quá xa server time (`ts_drift_ms`)
7. **`r`** khớp lại công thức digest

Endpoint debug hữu ích:

```http
POST /api/security/echo
```

Response có block `gauth` mô tả lỗi decode / match.

---

## 8. Ví dụ end-to-end (GET jobs)

### Request

```http
GET /api/advertising/publishers/instagram/jobs?instagram_account_id=966624&data=null
Authorization: Bearer <jwt>
g-device-id: 32484704-8a4e-4909-9d42-866773b321d6
g-username: vinhhacker
g-auth: Zmo3WFa5lp2X...
t: VFZSak5FMTZhekJPUkZGNVQwRTlQUT09
```

### Payload đã giải mã (thực tế từ browser)

```json
{
  "t": 1783944428398,
  "x": "z7Yv5E5ClYSc0gyyjllQ2w",
  "d": "32484704-8a4e-4909-9d42-866773b321d6",
  "u": 639111,
  "n": "GET",
  "k": "/api/advertising/publishers/instagram/jobs",
  "q": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "r": "286c4f5bd68ec1c4"
}
```

### Tính lại `q` và `r`

```text
q = SHA256("")
  = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

r = SHA256(
      "1783944428398"
      + ":" + "32484704-8a4e-4909-9d42-866773b321d6"
      + ":" + q
      + ":" + "glk-gauth-v3-2026q3"
    )[:16]
  = 286c4f5bd68ec1c4
```

---

## 9. Header `t` (không nằm trong AES)

```js
t = btoa(btoa(btoa(String(unix_seconds))))
```

Ví dụ:

```text
unix = 1783944424
→ btoa × 3 → "VFZSak5FMTZhekJPUkZGNVQwRTlQUT09"
```

Đây là timestamp phụ (giây), tách biệt field `t` (ms) bên trong `g-auth`.

---

## 10. Pseudocode Python

```python
import base64, hashlib, json, secrets, time
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

SALT = "glk-gauth-v3-2026q3"
INFO = "aes-gcm-key"

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

def sha256_hex(s: str) -> str:
    return hashlib.sha256(s.encode()).hexdigest()

def derive_key(signing_key_b64: str) -> bytes:
    ikm = base64.b64decode(signing_key_b64)
    return HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=SALT.encode(),
        info=INFO.encode(),
    ).derive(ikm)

def encrypt_g_auth(method, path, body, signing_key, device_id, user_id) -> str:
    key = derive_key(signing_key)
    iv = secrets.token_bytes(12)
    t = int(time.time() * 1000)
    q = sha256_hex(body if body is not None else "")
    r = sha256_hex(f"{t}:{device_id}:{q}:{SALT}")[:16]
    payload = {
        "t": t,
        "x": b64url(secrets.token_bytes(16)),
        "d": device_id,
        "u": user_id,
        "n": method.upper(),
        "k": path,
        "q": q,
        "r": r,
    }
    pt = json.dumps(payload, separators=(",", ":")).encode()
    ct = AESGCM(key).encrypt(iv, pt, None)  # ct + tag
    return b64url(iv + ct)

def decrypt_g_auth(token: str, signing_key: str) -> dict:
    key = derive_key(signing_key)
    pad = "=" * (-len(token) % 4)
    raw = base64.urlsafe_b64decode(token.replace("-", "+").replace("_", "/") + pad)
    iv, ct = raw[:12], raw[12:]
    pt = AESGCM(key).decrypt(iv, ct, None)
    return json.loads(pt)
```

---

## 11. Lỗi thường gặp

| Triệu chứng | Nguyên nhân |
|---|---|
| `decrypt_fail_...` | Sai `signing_key`, hỏng token, hoặc không đúng thuật toán HKDF/AES |
| `AUTH_MISSING` | Không gửi `g-auth` (và/hoặc header bắt buộc khác) |
| `403` “cập nhật phiên bản…” | Token thiếu/sai trên endpoint jobs; server từ chối client “cũ/không ký” |
| 200 ở browser, 403 ở script | Reuse `g-auth` cũ; hoặc path/body hash khác (space trong JSON, thiếu `/api`, …) |
| Decode local OK nhưng server fail | Key local ≠ key server đang expect; lấy lại từ `store.state.signing_key` |

---

## 12. Checklist implement đúng

1. `signing_key` = **store browser**, decode ra đúng 32 bytes
2. HKDF salt = `glk-gauth-v3-2026q3`, info = `aes-gcm-key`
3. Path ký = pathname có prefix `/api/...`, **không** query
4. GET body ký = `""`
5. POST body ký = **cùng bytes** body gửi đi (`JSON` compact nếu stringify)
6. Payload JSON **không space**, đúng thứ tự key `t,x,d,u,n,k,q,r`
7. AES-GCM IV 12 bytes, output `Base64URL(IV || CT||TAG)`
8. **Mỗi request** tạo `g-auth` mới

---

## 13. Tóm tắt một dòng

> **`g-auth` = Base64URL( IV₁₂ ‖ AES-256-GCM<sub>HKDF(signing_key)</sub>( JSON{method, path, bodyHash, device, user, time, nonce, digest} ) )**
