feat: RSA login encryption + confirmed wechat type codes

Replace the Phase-0 placeholder password encryption with real RSA
(PKCS#1 v1.5). encrypt_password now builds an RSA public key from the
getSecureKey modulus/exponent (RSAPublicNumbers, e=int(hex,16),
n=int(hex,16)), encrypts the UTF-8 password, and returns hex (documented
assumption -- base64 is the one-line fallback noted in the docstring).
get_secure_key now returns the inner data dict to match the new contract;
pc_login keeps {account, password} with a comment on the username/password
and account/pwd fallbacks if pcLogin rejects.

Extend normalize_wechat_type with the Phase-0 confirmed codes:
2 -> image, 3 -> image_legacy, 4 -> video, 10 -> system (1 -> text and
-2 -> divider unchanged; 6/9/13/15/21/22 stay unknown until confirmed).

Tests:
- encrypt_password round-trip: generate an RSA keypair, build secure_key
  from its public numbers, encrypt, then decrypt with the private key and
  assert the original password comes back. Pins 512-hex output for a
  2048-bit key. No network.
- Parser parametrize table extended for the new codes (and a downstream
  message-fixture assertion updated from unknown -> image_legacy).

Suite: 87 -> 97 passing. ruff check clean.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-01 13:37:42 +08:00
parent 7f19b12e74
commit eebff9f786
7 changed files with 214 additions and 102 deletions
+21 -18
View File
@@ -7,20 +7,20 @@ pipeline is built.
Auth modes (mutually exclusive; session mode preferred for now)
--------------------------------------------------------------
1. **Existing-session mode (preferred today).** Set ``YK_USERID``,
1. **Existing-session mode (most reliable).** Set ``YK_USERID``,
``YK_COMPANY``, and optionally ``YK_COOKIE``. The script builds a
:class:`YunkeClient` directly from that captured session
(``userid=`` / ``company=`` / ``cookies=``) and BYPASSES login. This is the
only auth mode that actually authenticates against the real CRM today —
see the Phase-0 note below.
(``userid=`` / ``company=`` / ``cookies=``) and BYPASSES login. This avoids
the remaining ``pcLogin`` body-field-name inference in mode 2.
2. **Login mode (future).** Set ``YUNKE_USERNAME`` and ``YUNKE_PASSWORD`` (and
2. **Login mode.** Set ``YUNKE_USERNAME`` and ``YUNKE_PASSWORD`` (and
leave the session vars unset). The script calls :meth:`YunkeClient.login`,
which runs the ``getSecureKey`` -> ``pcLogin`` flow. **NOTE:** password
encryption is a Phase-0 placeholder (see
:func:`app.session.login.encrypt_password`), so the real CRM will REJECT the
login. The script catches that, prints a clear message, and exits with code
3. Once Phase-0 login lands, this mode will just work.
which runs the ``getSecureKey`` -> ``pcLogin`` flow. Password encryption
is real RSA (PKCS#1 v1.5) using the ``getSecureKey`` modulus/exponent —
see :func:`app.session.login.encrypt_password`. The remaining inference
is the ``pcLogin`` request-body field names; if the real CRM rejects the
body, the script catches that, prints a clear message, and exits with
code 3.
If NEITHER set of env vars is present, the script prints usage and exits
with code 2.
@@ -51,7 +51,7 @@ Windows, ``source .venv/bin/activate`` on *nix)::
export YK_COOKIE="JSESSIONID=deadbeef; other=foo"
python -m scripts.smoke_wechat
# Login mode (Phase-0 blocked — will exit 3 with a message):
# Login mode (real RSA encryption; pcLogin body field names inferred):
export YUNKE_USERNAME=user
export YUNKE_PASSWORD=pass
python -m scripts.smoke_wechat
@@ -61,8 +61,9 @@ The smoke DB defaults to ``./smoke.db`` (SQLite); override the URL with
SQLAlchemy async URL). On a 401 / expired session, the script prints a helpful
message and exits non-zero (code 4) instead of dumping a traceback.
Exit codes: ``0`` success · ``2`` no auth env · ``3`` login failed (Phase-0
blocked) · ``4`` upstream auth/session error mid-scrape.
Exit codes: ``0`` success · ``2`` no auth env · ``3`` login failed (e.g. an
inferred ``pcLogin`` body field name the real CRM rejects) · ``4`` upstream
auth/session error mid-scrape.
"""
from __future__ import annotations
@@ -195,17 +196,19 @@ async def main() -> int:
_print_usage()
return 2
# Login mode: try to log in. Phase-0 placeholder encryption will be
# rejected by the real CRM — catch and explain instead of tracebacking.
# Login mode: try to log in. The pcLogin body field names are still an
# inference, so a real-CRM rejection is possible — catch and explain
# instead of tracebacking.
if not client.logged_in:
try:
await client.login()
except Exception as exc: # noqa: BLE001 — any failure is fatal-but-explained
sys.stderr.write(
f"\n[smoke] login failed (likely Phase-0 placeholder "
f"encryption): {exc!r}\n"
f"\n[smoke] login failed (real RSA encryption is in place; "
f"the likely cause is an inferred pcLogin body field name): "
f"{exc!r}\n"
"Use existing-session mode (YK_USERID / YK_COMPANY / YK_COOKIE) "
"until Phase-0 login lands.\n"
"if login keeps failing.\n"
)
await client.aclose()
return 3