From afe389ce7ea616abe7e86fc101d264ad2e348c11 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Tue, 31 Mar 2026 16:33:07 +0400 Subject: [PATCH] Research: EC-SRP5 authentication fully reverse-engineered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key findings: - btest EC-SRP5 uses [len][payload] framing (NO 0x06 handler byte) - Winbox uses [len][0x06][payload] — that one byte was the difference - Crypto is identical: Curve25519 Weierstrass, SHA256, SRP-like key exchange - Python prototype successfully authenticates against MikroTik RouterOS 7.x Files: - docs/ecsrp5-research.md: complete protocol spec, captured exchange, impl plan - proto-test/btest_ecsrp5_client.py: working Python EC-SRP5 btest client - proto-test/btest_mitm.py: MITM proxy used to discover the framing - proto-test/elliptic_curves.py: Curve25519 Weierstrass (from MarginResearch) Based on MarginResearch/mikrotik_authentication (MIT License). Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 2 + docs/architecture.md | 13 +- docs/ecsrp5-research.md | 238 ++++++++++++++++++++++++++++++ proto-test/btest_ecsrp5_client.py | 235 +++++++++++++++++++++++++++++ proto-test/btest_mitm.py | 188 +++++++++++++++++++++++ proto-test/elliptic_curves.py | 138 +++++++++++++++++ 6 files changed, 812 insertions(+), 2 deletions(-) create mode 100644 docs/ecsrp5-research.md create mode 100644 proto-test/btest_ecsrp5_client.py create mode 100644 proto-test/btest_mitm.py create mode 100644 proto-test/elliptic_curves.py diff --git a/.gitignore b/.gitignore index 911faa0..40c191c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ btest_original .claude/ .env +proto-test/venv/ +proto-test/__pycache__/ diff --git a/docs/architecture.md b/docs/architecture.md index 56c1ebd..f13766c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -50,12 +50,20 @@ sequenceDiagram alt No auth configured SRV->>TCP: AUTH_OK [01 00 00 00] - else MD5 auth + else MD5 auth (RouterOS < 6.43) SRV->>TCP: AUTH_REQUIRED [02 00 00 00] SRV->>TCP: Challenge [16 random bytes] MK->>TCP: Response [16 hash + 32 username] Note over SRV: Verify MD5(pass + MD5(pass + challenge)) SRV->>TCP: AUTH_OK or AUTH_FAILED + else EC-SRP5 auth (RouterOS >= 6.43) + SRV->>TCP: EC-SRP5 [03 00 00 00] + MK->>TCP: [len][username\0][client_pubkey:32][parity:1] + SRV->>TCP: [len][server_pubkey:32][parity:1][salt:16] + MK->>TCP: [len][client_proof:32] + SRV->>TCP: [len][server_proof:32] + Note over SRV: Curve25519 Weierstrass EC-SRP5
See docs/ecsrp5-research.md + SRV->>TCP: AUTH_OK [01 00 00 00] end alt TCP mode @@ -178,7 +186,7 @@ btest-rs/ │ ├── main.rs # CLI entry point, argument parsing │ ├── lib.rs # Public API (used by integration tests) │ ├── protocol.rs # Wire format: Command, StatusMessage, constants -│ ├── auth.rs # MD5 challenge-response authentication +│ ├── auth.rs # Authentication (MD5 + EC-SRP5) │ ├── server.rs # Server mode: listener, TCP/UDP handlers │ ├── client.rs # Client mode: connector, TCP/UDP handlers │ └── bandwidth.rs # Rate limiting, formatting, shared state @@ -193,6 +201,7 @@ btest-rs/ ├── docs/ │ ├── architecture.md # This file │ ├── protocol.md # Protocol specification +│ ├── ecsrp5-research.md # EC-SRP5 reverse-engineering findings │ ├── user-guide.md # Usage documentation │ └── docker.md # Docker & deployment guide ├── Dockerfile # Production Docker image diff --git a/docs/ecsrp5-research.md b/docs/ecsrp5-research.md new file mode 100644 index 0000000..c9a6708 --- /dev/null +++ b/docs/ecsrp5-research.md @@ -0,0 +1,238 @@ +# EC-SRP5 Authentication Research + +## Summary + +MikroTik RouterOS >= 6.43 uses EC-SRP5 (Elliptic Curve Secure Remote Password) for authentication. When the btest server has auth enabled, it responds with `03 00 00 00` instead of `02 00 00 00` (legacy MD5). + +**Status: Fully reverse-engineered and verified.** Python prototype authenticates successfully against MikroTik RouterOS 7.x btest server. + +## Discovery Process + +### Step 1: Initial Capture + +Connected our client to MikroTik btest server with auth enabled. Server responded with `03 00 00 00` and waited for the client to initiate. + +### Step 2: Winbox EC-SRP5 Verification + +Tested the EC-SRP5 crypto implementation (from [MarginResearch/mikrotik_authentication](https://github.com/MarginResearch/mikrotik_authentication)) against MikroTik's Winbox port (8291). **Authentication succeeded**, confirming the elliptic curve math is correct. + +### Step 3: Framing Discovery via MITM + +The Winbox `[len][0x06][payload]` framing was rejected by the btest port. To discover the correct framing, we built a MITM proxy (`proto-test/btest_mitm.py`) and routed a MikroTik client through it to the MikroTik server. + +**Finding: btest uses `[len][payload]` — identical to Winbox but without the `0x06` handler byte.** + +### Step 4: Successful Authentication + +Updated the Python prototype to use `[len][payload]` framing. EC-SRP5 authentication against MikroTik's btest server succeeded and data transfer began. + +## Protocol Specification + +### Auth Trigger + +After the standard btest handshake (HELLO + Command), the server responds: + +``` +01 00 00 00 → No auth required +02 00 00 00 → MD5 challenge-response (RouterOS < 6.43) +03 00 00 00 → EC-SRP5 (RouterOS >= 6.43) +``` + +### EC-SRP5 Handshake (4 messages after `03 00 00 00`) + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + Note over S: Server sent 03 00 00 00 + + C->>S: MSG1: [len][username\0][client_pubkey:32][parity:1] + Note over C: len = 1 byte, total = len + 1 bytes + + S->>C: MSG2: [len][server_pubkey:32][parity:1][salt:16] + Note over S: len = 49 (0x31) + + C->>S: MSG3: [len][client_confirmation:32] + Note over C: len = 32 (0x20) + + S->>C: MSG4: [len][server_confirmation:32] + Note over S: len = 32 (0x20) + + Note over S: Then continues with normal btest flow: + S->>C: AUTH_OK [01 00 00 00] + S->>C: UDP port [2 bytes BE] (if UDP mode) +``` + +### Framing Comparison + +| Protocol | Message framing | +|----------|----------------| +| Winbox (port 8291) | `[len:1][0x06][payload]` | +| **btest (port 2000)** | **`[len:1][payload]`** | +| MAC Telnet (UDP 20561) | Control packets with magic bytes | + +The `0x06` handler byte in Winbox identifies the message as an auth message. Btest omits it since the auth context is implicit after `03 00 00 00`. + +### Captured Exchange (from MITM) + +``` +CLIENT → SERVER (40 bytes): + 27 61 6e 74 61 72 00 38 8a 37 36 52 6a 32 e9 87 'antar.8.76Rj2.. + 4e 92 f8 c3 aa a1 18 da cd 71 b6 ab 76 fd 72 aa N........q..v.r. + c3 f6 6a 43 9b c8 a1 01 ..jC.... + + Decoded: + len=0x27 (39 bytes payload) + username="antar\0" + pubkey=388a373652...c8a1 (32 bytes) + parity=0x01 + +SERVER → CLIENT (50 bytes): + 31 6c c9 e3 1a 79 43 4a 40 51 de fd 55 cc 8d 6d 1l...yCJ@Q..U..m + 3c ec cd 73 19 1f a6 83 15 94 62 52 97 fe 5d 89 <..s......bR..]. + 1a 00 3c ec 65 b8 34 28 0a 16 c5 48 0d 7b 50 00 ..<.e.4(...H.{P. + e3 80 .. + + Decoded: + len=0x31 (49 bytes payload) + server_pubkey=6cc9e31a...5d891a (32 bytes) + parity=0x00 + salt=3cec65b834280a16c5480d7b5000e380 (16 bytes) + +CLIENT → SERVER (33 bytes): + 20 9b 1f 74 ec 40 31 2c ... + + Decoded: + len=0x20 (32 bytes payload) + client_cc=9b1f74ec... (32 bytes, SHA256 proof) + +SERVER → CLIENT (33 bytes): + 20 7d 59 b3 2e 28 6e 52 ... + + Decoded: + len=0x20 (32 bytes payload) + server_cc=7d59b32e... (32 bytes, SHA256 proof) + +POST-AUTH: + 01 00 00 00 07 fa + + Decoded: + AUTH_OK=01000000 + UDP_port=0x07fa (2042) +``` + +## Cryptographic Details + +### Elliptic Curve: Curve25519 in Weierstrass Form + +``` +p = 2^255 - 19 +r = curve order (same as Ed25519) +Montgomery A = 486662 + +Weierstrass conversion: + a = 0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144 + b = 0x7b425ed097b425ed097b425ed097b425ed097b425ed097b4260b5e9c7710c864 + +Generator: lift_x(9) in Montgomery, converted to Weierstrass +Cofactor: 8 +``` + +All EC math is in Weierstrass form. Public keys are transmitted as Montgomery x-coordinates (32 bytes big-endian) plus a 1-byte y-parity flag. + +### Key Derivation + +``` +inner = SHA256(username + ":" + password) +validator_priv (i) = SHA256(salt || inner) +validator_pub (x_gamma) = i * G +``` + +### Shared Secret Computation + +**Client side (ECPESVDP-SRP-A):** +``` +v = redp1(x_gamma, parity=1) # hash-to-curve of validator pubkey +w_b = lift_x(server_pubkey) + v # undo verifier blinding +j = SHA256(client_pubkey || server_pubkey) +scalar = (i * j + s_a) mod r # combined scalar +Z = scalar * w_b # shared secret point +z = to_montgomery(Z).x # Montgomery x-coordinate +``` + +**Server side (ECPESVDP-SRP-B):** +``` +gamma = redp1(x_gamma, parity=0) +w_a = lift_x(client_pubkey) +Z = s_b * (w_a + j * gamma) # where j = SHA256(x_w_a || x_w_b) +z = to_montgomery(Z).x +``` + +### Confirmation Codes + +``` +client_cc = SHA256(j || z) +server_cc = SHA256(j || client_cc || z) +``` + +Both sides verify the peer's confirmation code to ensure the shared secret matches. + +### redp1 (Hash-to-Curve) + +```python +def redp1(x_bytes, parity): + x = SHA256(x_bytes) + while True: + x2 = SHA256(x) + point = lift_x(int(x2), parity) + if point is valid: + return point + x = (int(x) + 1).to_bytes(32) +``` + +## Implementation Plan for Rust + +### Required Crates + +| Crate | Purpose | +|-------|---------| +| `num-bigint` + `num-traits` | Big integer arithmetic for field operations | +| `sha2` | SHA-256 | +| `ecdsa` or custom | Curve25519 Weierstrass point operations | + +**Note:** `curve25519-dalek` operates in Montgomery/Edwards form, not Weierstrass. We need Weierstrass arithmetic for compatibility with MikroTik's implementation. Options: +1. Use `num-bigint` for direct field arithmetic (like the Python `ecdsa` library) +2. Use the `p256` crate's infrastructure with custom curve parameters +3. Port the Python `WCurve` class directly using big integers + +### Implementation Steps + +1. **Port `WCurve`** — Weierstrass curve with Curve25519 parameters, point multiplication, `lift_x`, `redp1`, Montgomery conversion +2. **Port EC-SRP5 client** — generate keypair, compute shared secret, confirmation codes +3. **Port EC-SRP5 server** — verify client proof, generate server proof (for our server mode) +4. **Integrate into `auth.rs`** — handle `03 00 00 00` response with btest-specific `[len][payload]` framing +5. **Server registration** — derive salt + validator from username/password for server-side verification + +### Server-Side Specifics + +When our server receives a client with EC-SRP5 support, we need to: +1. Store `salt` and `x_gamma` (validator public key) per user — derived from username + password at startup +2. Generate ephemeral server keypair +3. Compute password-entangled public key: `W_b = s_b * G + redp1(x_gamma, 0)` +4. Verify client's confirmation code +5. Send server confirmation code + +## Files + +| File | Purpose | +|------|---------| +| `proto-test/elliptic_curves.py` | Curve25519 Weierstrass implementation | +| `proto-test/btest_ecsrp5_client.py` | Working Python btest EC-SRP5 client | +| `proto-test/btest_mitm.py` | MITM proxy for protocol analysis | + +## Credits + +- **[MarginResearch](https://github.com/MarginResearch/mikrotik_authentication)** — Reverse-engineered MikroTik's EC-SRP5 for Winbox/MAC Telnet +- **[Margin Research blog](https://margin.re/2022/02/mikrotik-authentication-revealed/)** — Detailed write-up of MikroTik authentication +- **btest framing discovery** — MITM analysis showing btest uses `[len][payload]` (no `0x06` handler byte) diff --git a/proto-test/btest_ecsrp5_client.py b/proto-test/btest_ecsrp5_client.py new file mode 100644 index 0000000..d93ce4f --- /dev/null +++ b/proto-test/btest_ecsrp5_client.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +""" +btest EC-SRP5 authentication test client. +Connects to a MikroTik btest server (RouterOS >= 6.43) and performs +the EC-SRP5 handshake to authenticate. + +Usage: + python3 btest_ecsrp5_client.py -a 172.16.81.1 -u admin -p password + python3 btest_ecsrp5_client.py -a 172.16.81.1 -u admin -p password --receive +""" +import socket +import secrets +import hashlib +import argparse +import struct +import sys +import time + +import elliptic_curves + +BTEST_PORT = 2000 + + +def sha256(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +def hexdump(label: str, data: bytes): + print(f" {label} ({len(data)} bytes): {data.hex()}") + + +class BtestECSRP5Client: + def __init__(self, host: str, port: int = BTEST_PORT): + self.host = host + self.port = port + self.w = elliptic_curves.WCurve() + self.sock = None + + def connect(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(10) + self.sock.connect((self.host, self.port)) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + print(f"Connected to {self.host}:{self.port}") + + def recv_exact(self, n: int) -> bytes: + buf = b"" + while len(buf) < n: + chunk = self.sock.recv(n - len(buf)) + if not chunk: + raise ConnectionError("Connection closed") + buf += chunk + return buf + + def do_hello_and_command(self, proto=1, direction=2, conn_count=0, tx_size=0x8000): + """Send command, return auth response type.""" + # Receive HELLO + hello = self.recv_exact(4) + hexdump("HELLO", hello) + assert hello == b"\x01\x00\x00\x00", f"Bad HELLO: {hello.hex()}" + + # Send command + cmd = struct.pack("= 6.43)") + return self.do_ecsrp5_auth(username, password) + else: + print(f"Unknown auth response: {resp.hex()}") + return False + + +def main(): + parser = argparse.ArgumentParser(description="btest EC-SRP5 auth test client") + parser.add_argument("-a", "--address", required=True, help="MikroTik IP address") + parser.add_argument("-u", "--username", default="admin", help="Username") + parser.add_argument("-p", "--password", default="", help="Password") + parser.add_argument("-P", "--port", type=int, default=BTEST_PORT, help="Port") + parser.add_argument("--receive", action="store_true", help="Test receive direction") + args = parser.parse_args() + + direction = "receive" if args.receive else "receive" + + client = BtestECSRP5Client(args.address, args.port) + try: + success = client.run_test(args.username, args.password, direction) + if success: + print("\nAuth passed! Reading some data...") + try: + data = client.sock.recv(4096) + hexdump("First data received", data[:64]) + print(f" (total {len(data)} bytes)") + + # Read for a few seconds to confirm data flows + client.sock.settimeout(2) + total = len(data) + for _ in range(5): + try: + data = client.sock.recv(65536) + total += len(data) + except socket.timeout: + break + print(f"\nTotal received: {total} bytes ({total * 8 / 1_000_000:.2f} Mbps over ~2s)") + except Exception as e: + print(f"Data read error: {e}") + sys.exit(0 if success else 1) + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/proto-test/btest_mitm.py b/proto-test/btest_mitm.py new file mode 100644 index 0000000..44ac107 --- /dev/null +++ b/proto-test/btest_mitm.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +MITM proxy for btest protocol. +Sits between MikroTik client and MikroTik server, logs all bytes exchanged. + +Usage: + python3 btest_mitm.py --listen 2000 --target 172.16.81.1:2000 + +Then point MikroTik client at THIS machine's IP on port 2000. +""" +import socket +import select +import sys +import argparse +import time +import threading + + +def hexdump_line(data, offset=0): + """Format a single line of hex dump.""" + hex_part = " ".join(f"{b:02x}" for b in data) + ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in data) + return f" {offset:04x} {hex_part:<48s} {ascii_part}" + + +def hexdump(label, data): + """Pretty hex dump with ASCII.""" + ts = time.strftime("%H:%M:%S", time.localtime()) + print(f"\n[{ts}] {label} ({len(data)} bytes)") + for i in range(0, len(data), 16): + print(hexdump_line(data[i : i + 16], i)) + sys.stdout.flush() + + +def annotate_btest(direction, data, state): + """Try to annotate known btest protocol messages.""" + if len(data) == 4: + val = data.hex() + if val == "01000000": + return "HELLO / AUTH_OK" + elif val == "02000000": + return "AUTH_REQUIRED (MD5)" + elif val == "03000000": + state["auth_type"] = "ecsrp5" + return "AUTH_REQUIRED (EC-SRP5)" + elif val == "00000000": + return "AUTH_FAILED" + + if len(data) == 16 and state.get("stage") == "command": + proto = "UDP" if data[0] == 0 else "TCP" + dirs = {1: "RX", 2: "TX", 3: "BOTH"} + d = dirs.get(data[1], f"0x{data[1]:02x}") + conn = data[3] + return f"COMMAND: proto={proto} dir={d} conn_count={conn}" + + if len(data) == 2 and state.get("auth_type") == "ecsrp5": + return f"UDP port assignment: {int.from_bytes(data, 'big')}" + + if state.get("auth_type") == "ecsrp5" and state.get("stage") == "ecsrp5": + if len(data) >= 33: + return f"EC-SRP5 message (likely pubkey + parity/salt)" + + return None + + +def proxy_connection(client_sock, client_addr, target_host, target_port): + """Handle one proxied connection.""" + print(f"\n{'='*60}") + print(f"New connection from {client_addr[0]}:{client_addr[1]}") + print(f"Proxying to {target_host}:{target_port}") + print(f"{'='*60}") + + try: + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_sock.settimeout(30) + server_sock.connect((target_host, target_port)) + server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except Exception as e: + print(f"Failed to connect to target: {e}") + client_sock.close() + return + + state = {"stage": "hello", "msg_count": 0} + + try: + while True: + readable, _, _ = select.select( + [client_sock, server_sock], [], [], 30 + ) + + if not readable: + print("\n[TIMEOUT] No data for 30 seconds") + break + + for sock in readable: + if sock is server_sock: + data = server_sock.recv(65536) + if not data: + print("\n[SERVER CLOSED]") + return + direction = "SERVER → CLIENT" + annotation = annotate_btest("s2c", data, state) + hexdump(direction, data) + if annotation: + print(f" >>> {annotation}") + + # Track state + if state["stage"] == "hello" and len(data) == 4: + state["stage"] = "command" + elif state["stage"] == "auth_resp": + state["stage"] = "ecsrp5" + + # Forward to client + client_sock.sendall(data) + + elif sock is client_sock: + data = client_sock.recv(65536) + if not data: + print("\n[CLIENT CLOSED]") + return + direction = "CLIENT → SERVER" + annotation = annotate_btest("c2s", data, state) + hexdump(direction, data) + if annotation: + print(f" >>> {annotation}") + + # Track state + if state["stage"] == "command" and len(data) == 16: + state["stage"] = "auth_resp" + state["msg_count"] += 1 + + # Forward to server + server_sock.sendall(data) + + except (ConnectionResetError, BrokenPipeError) as e: + print(f"\n[CONNECTION ERROR] {e}") + except Exception as e: + print(f"\n[ERROR] {e}") + import traceback + traceback.print_exc() + finally: + client_sock.close() + server_sock.close() + print(f"\n{'='*60}") + print(f"Connection closed ({state['msg_count']} client messages)") + print(f"{'='*60}") + + +def main(): + parser = argparse.ArgumentParser(description="btest MITM proxy") + parser.add_argument( + "-l", "--listen", type=int, default=2000, help="Listen port (default: 2000)" + ) + parser.add_argument( + "-t", + "--target", + required=True, + help="Target MikroTik address:port (e.g., 172.16.81.1:2000)", + ) + args = parser.parse_args() + + target_parts = args.target.split(":") + target_host = target_parts[0] + target_port = int(target_parts[1]) if len(target_parts) > 1 else 2000 + + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + listener.bind(("0.0.0.0", args.listen)) + listener.listen(5) + + print(f"MITM proxy listening on 0.0.0.0:{args.listen}") + print(f"Forwarding to {target_host}:{target_port}") + print(f"Point MikroTik client at this machine's IP, port {args.listen}") + print() + + while True: + client_sock, client_addr = listener.accept() + client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + t = threading.Thread( + target=proxy_connection, + args=(client_sock, client_addr, target_host, target_port), + daemon=True, + ) + t.start() + + +if __name__ == "__main__": + main() diff --git a/proto-test/elliptic_curves.py b/proto-test/elliptic_curves.py new file mode 100644 index 0000000..05e7ff1 --- /dev/null +++ b/proto-test/elliptic_curves.py @@ -0,0 +1,138 @@ +""" +Elliptic curve implementation for MikroTik EC-SRP5 authentication. +Based on MarginResearch/mikrotik_authentication (MIT License). +Curve25519 in Weierstrass form. +""" +import hashlib +import ecdsa + + +def _egcd(a, b): + if a == 0: + return (b, 0, 1) + else: + g, y, x = _egcd(b % a, a) + return (g, x - (b // a) * y, y) + + +def _modinv(a: int, p: int): + if a < 0: + a = a % p + g, x, y = _egcd(a, p) + if g != 1: + raise Exception("modular inverse does not exist") + return x % p + + +def _legendre_symbol(a: int, p: int): + l = pow(a, (p - 1) // 2, p) + if l == p - 1: + return -1 + return l + + +def _prime_mod_sqrt(a: int, p: int): + a %= p + if a == 0: + return [0] + if p == 2: + return [a] + if _legendre_symbol(a, p) != 1: + return [] + if p % 4 == 3: + x = pow(a, (p + 1) // 4, p) + return [x, p - x] + + q, s = p - 1, 0 + while q % 2 == 0: + s += 1 + q //= 2 + + z = 1 + while _legendre_symbol(z, p) != -1: + z += 1 + c = pow(z, q, p) + + x = pow(a, (q + 1) // 2, p) + t = pow(a, q, p) + m = s + while t != 1: + i, e = 0, 2 + for i in range(1, m): + if pow(t, e, p) == 1: + break + e *= 2 + b = pow(c, 2 ** (m - i - 1), p) + x = (x * b) % p + t = (t * b * b) % p + c = (b * b) % p + m = i + + return [x, p - x] + + +class WCurve: + def __init__(self): + self.__p = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED + self.__r = 0x1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED + self.__mont_a = 486662 + self.__conversion_from_m = self.__mont_a * _modinv(3, self.__p) % self.__p + self.__conversion = (self.__p - self.__mont_a * _modinv(3, self.__p)) % self.__p + self.__a = 0x2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144 + self.__b = 0x7B425ED097B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864 + self.__h = 8 + self.__curve = ecdsa.ellipticcurve.CurveFp(self.__p, self.__a, self.__b, self.__h) + self.__g = self.lift_x(9, 0) + + def gen_public_key(self, priv: bytes): + assert len(priv) == 32 + priv = int.from_bytes(priv, "big") + pt = priv * self.__g + return self.to_montgomery(pt) + + def to_montgomery(self, pt): + x = (pt.x() + self.__conversion) % self.__p + return int(x).to_bytes(32, "big"), pt.y() & 1 + + def lift_x(self, x: int, parity: bool): + x = x % self.__p + y_squared = (x**3 + self.__mont_a * x**2 + x) % self.__p + x += self.__conversion_from_m + x %= self.__p + ys = _prime_mod_sqrt(y_squared, self.__p) + if ys != []: + pt1 = ecdsa.ellipticcurve.PointJacobi(self.__curve, x, ys[0], 1, self.__r) + pt2 = ecdsa.ellipticcurve.PointJacobi(self.__curve, x, ys[1], 1, self.__r) + if pt1.y() & 1 == 1 and parity != 0: + return pt1 + elif pt2.y() & 1 == 1 and parity != 0: + return pt2 + elif pt1.y() & 1 == 0 and parity == 0: + return pt1 + else: + return pt2 + else: + return -1 + + def redp1(self, x: bytes, parity: bool): + x = hashlib.sha256(x).digest() + while True: + x2 = hashlib.sha256(x).digest() + pt = self.lift_x(int.from_bytes(x2, "big"), parity) + if pt == -1: + x = (int.from_bytes(x, "big") + 1).to_bytes(32, "big") + else: + break + return pt + + def gen_password_validator_priv(self, username: str, password: str, salt: bytes): + assert len(salt) == 0x10 + return hashlib.sha256( + salt + hashlib.sha256((username + ":" + password).encode("utf-8")).digest() + ).digest() + + def multiply_by_g(self, a: int): + return a * self.__g + + def finite_field_value(self, a: int): + return a % self.__r