终局
This commit is contained in:
105
src/lib.rs
105
src/lib.rs
@@ -5,3 +5,108 @@ pub mod gift256;
|
||||
pub mod hash;
|
||||
pub mod solver;
|
||||
pub mod feistel;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct JwtPayload {
|
||||
#[serde(default)]
|
||||
f: u32,
|
||||
#[serde(default)]
|
||||
s: u32,
|
||||
#[serde(default)]
|
||||
t: String,
|
||||
d: String,
|
||||
#[serde(default)]
|
||||
l: String,
|
||||
#[serde(default)]
|
||||
i: String,
|
||||
#[serde(default)]
|
||||
e: u64,
|
||||
#[serde(default)]
|
||||
n: String,
|
||||
#[serde(default)]
|
||||
c: u32,
|
||||
}
|
||||
|
||||
/// Takes a raw JWT string, runs the full PoW pipeline, returns the base64 proof.
|
||||
pub fn solve_jwt(jwt_req: &str) -> Result<String, String> {
|
||||
// 1. Decode JWT payload (base64url, no signature verification)
|
||||
let parts: Vec<&str> = jwt_req.split('.').collect();
|
||||
if parts.len() < 2 {
|
||||
return Err("Invalid JWT format".into());
|
||||
}
|
||||
let payload_b64 = parts[1];
|
||||
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(payload_b64)
|
||||
.map_err(|e| format!("Invalid base64 in JWT payload: {e}"))?;
|
||||
let payload: JwtPayload = serde_json::from_slice(&payload_bytes)
|
||||
.map_err(|e| format!("Invalid JSON in JWT payload: {e}"))?;
|
||||
|
||||
// 2. Decode challenge data from `d` field
|
||||
let challenge_data = if let Some(eq_pos) = payload.d.find('=') {
|
||||
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
|
||||
let part1 = &payload.d[..split_pos];
|
||||
let part2 = &payload.d[split_pos..];
|
||||
|
||||
let mut data = STANDARD.decode(part1)
|
||||
.map_err(|e| format!("Invalid base64 in challenge part1: {e}"))?;
|
||||
if !part2.is_empty() {
|
||||
let p2 = STANDARD.decode(part2).unwrap_or_else(|_| {
|
||||
let padded = format!("{}{}", part2, "=".repeat((4 - part2.len() % 4) % 4));
|
||||
STANDARD.decode(&padded).expect("Invalid base64 in challenge part2")
|
||||
});
|
||||
data.extend_from_slice(&p2);
|
||||
}
|
||||
data
|
||||
} else {
|
||||
STANDARD.decode(&payload.d)
|
||||
.map_err(|e| format!("Invalid base64 in challenge data: {e}"))?
|
||||
};
|
||||
|
||||
// 3. Split into encrypted_payload and seed_bytes
|
||||
let (encrypted_payload, seed_bytes) = if let Some(eq_pos) = payload.d.find('=') {
|
||||
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
|
||||
let part1_len = STANDARD.decode(&payload.d[..split_pos])
|
||||
.map_err(|e| format!("Invalid base64 in part1: {e}"))?.len();
|
||||
(&challenge_data[..part1_len], &challenge_data[part1_len..])
|
||||
} else {
|
||||
(challenge_data.as_slice(), &[] as &[u8])
|
||||
};
|
||||
|
||||
// 4. Feistel decrypt
|
||||
if encrypted_payload.len() < 33 {
|
||||
return Err(format!(
|
||||
"Encrypted payload too short: {} bytes (need >= 33)",
|
||||
encrypted_payload.len()
|
||||
));
|
||||
}
|
||||
|
||||
let (key_material, _control) = feistel::feistel_decrypt(encrypted_payload, 0);
|
||||
|
||||
let difficulty = if payload.c > 0 { payload.c } else { 1000 };
|
||||
|
||||
let challenge = solver::Challenge {
|
||||
key_material,
|
||||
difficulty,
|
||||
};
|
||||
|
||||
// 5. Derive PCG seed from seed_bytes
|
||||
let seed = if seed_bytes.len() >= 8 {
|
||||
u64::from_le_bytes([
|
||||
seed_bytes[0], seed_bytes[1], seed_bytes[2], seed_bytes[3],
|
||||
seed_bytes[4], seed_bytes[5], seed_bytes[6], seed_bytes[7],
|
||||
])
|
||||
} else {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64
|
||||
};
|
||||
|
||||
// 6. Solve and encode
|
||||
let proof = solver::solve(&challenge, seed);
|
||||
Ok(STANDARD.encode(&proof))
|
||||
}
|
||||
|
||||
146
src/main.rs
146
src/main.rs
@@ -1,132 +1,28 @@
|
||||
//! hCaptcha PoW solver CLI
|
||||
//! Parses JWT challenge -> calls solver -> outputs base64 proof
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use axum::{extract::Json, http::StatusCode, response::IntoResponse, routing::post, Router};
|
||||
use serde::Deserialize;
|
||||
|
||||
mod pcg;
|
||||
mod sbox;
|
||||
mod gift256;
|
||||
mod hash;
|
||||
mod solver;
|
||||
mod util;
|
||||
mod feistel;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct JwtPayload {
|
||||
#[serde(default)]
|
||||
f: u32,
|
||||
#[serde(default)]
|
||||
s: u32,
|
||||
#[serde(default)]
|
||||
t: String,
|
||||
d: String,
|
||||
#[serde(default)]
|
||||
l: String,
|
||||
#[serde(default)]
|
||||
i: String,
|
||||
#[serde(default)]
|
||||
e: u64,
|
||||
#[serde(default)]
|
||||
n: String,
|
||||
#[serde(default)]
|
||||
c: u32,
|
||||
struct SolveRequest {
|
||||
jwt: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// 1. Read JWT from command line
|
||||
let jwt_req = std::env::args().nth(1).expect("Usage: hcaptcha-pow <jwt>");
|
||||
|
||||
// 2. Decode JWT payload (base64url, no signature verification)
|
||||
let parts: Vec<&str> = jwt_req.split('.').collect();
|
||||
if parts.len() < 2 {
|
||||
eprintln!("Invalid JWT format");
|
||||
std::process::exit(1);
|
||||
async fn solve_handler(Json(req): Json<SolveRequest>) -> impl IntoResponse {
|
||||
let jwt = req.jwt;
|
||||
match tokio::task::spawn_blocking(move || hcaptcha_pow::solve_jwt(&jwt)).await {
|
||||
Ok(Ok(proof)) => (StatusCode::OK, Json(serde_json::json!({ "n": proof }))),
|
||||
Ok(Err(e)) => (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": e }))),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": format!("Task panicked: {e}") })),
|
||||
),
|
||||
}
|
||||
let payload_b64 = parts[1];
|
||||
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(payload_b64)
|
||||
.expect("Invalid base64 in JWT payload");
|
||||
let payload: JwtPayload = serde_json::from_slice(&payload_bytes).expect("Invalid JSON in JWT payload");
|
||||
|
||||
println!("Algorithm: {}", payload.n);
|
||||
println!("Difficulty: {}", payload.c);
|
||||
println!("Expiration: {}", payload.e);
|
||||
|
||||
// 3. Decode challenge data from `d` field
|
||||
// The `d` field is two concatenated base64 strings:
|
||||
// encrypted_payload (with = padding) + seed (no padding)
|
||||
// Split at the padding boundary and decode each part separately.
|
||||
let challenge_data = if let Some(eq_pos) = payload.d.find('=') {
|
||||
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
|
||||
let part1 = &payload.d[..split_pos];
|
||||
let part2 = &payload.d[split_pos..];
|
||||
|
||||
let mut data = STANDARD.decode(part1).expect("Invalid base64 in challenge part1");
|
||||
if !part2.is_empty() {
|
||||
let p2 = STANDARD.decode(part2).unwrap_or_else(|_| {
|
||||
// part2 may lack padding — add it
|
||||
let padded = format!("{}{}", part2, "=".repeat((4 - part2.len() % 4) % 4));
|
||||
STANDARD.decode(&padded).expect("Invalid base64 in challenge part2")
|
||||
});
|
||||
data.extend_from_slice(&p2);
|
||||
}
|
||||
data
|
||||
} else {
|
||||
STANDARD.decode(&payload.d).expect("Invalid base64 in challenge data")
|
||||
};
|
||||
|
||||
// Split challenge_data into encrypted_payload (part1) and seed_bytes (part2)
|
||||
// Part1 = first decode (203 bytes), Part2 = second decode (12 bytes)
|
||||
let (encrypted_payload, seed_bytes) = if let Some(eq_pos) = payload.d.find('=') {
|
||||
let split_pos = eq_pos + payload.d[eq_pos..].chars().take_while(|&c| c == '=').count();
|
||||
let part1_len = STANDARD.decode(&payload.d[..split_pos])
|
||||
.expect("Invalid base64 in part1").len();
|
||||
(&challenge_data[..part1_len], &challenge_data[part1_len..])
|
||||
} else {
|
||||
(challenge_data.as_slice(), &[] as &[u8])
|
||||
};
|
||||
|
||||
println!("Encrypted payload: {} bytes, Seed: {} bytes", encrypted_payload.len(), seed_bytes.len());
|
||||
|
||||
// 4. Feistel decrypt 33 bytes → 32 byte key + 1 byte control
|
||||
if encrypted_payload.len() < 33 {
|
||||
eprintln!("Encrypted payload too short: {} bytes (need >= 33)", encrypted_payload.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let (key_material, control) = feistel::feistel_decrypt(encrypted_payload, 0);
|
||||
println!("Feistel decrypted: key[0..4]={:02x}{:02x}{:02x}{:02x}, control=0x{:02x}",
|
||||
key_material[0], key_material[1], key_material[2], key_material[3], control);
|
||||
|
||||
let difficulty = if payload.c > 0 { payload.c } else { 1000 };
|
||||
|
||||
let challenge = solver::Challenge {
|
||||
key_material,
|
||||
difficulty,
|
||||
};
|
||||
|
||||
// 5. Derive PCG seed from seed_bytes (part2 of d)
|
||||
let seed = if seed_bytes.len() >= 8 {
|
||||
u64::from_le_bytes([
|
||||
seed_bytes[0], seed_bytes[1], seed_bytes[2], seed_bytes[3],
|
||||
seed_bytes[4], seed_bytes[5], seed_bytes[6], seed_bytes[7],
|
||||
])
|
||||
} else {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u64
|
||||
};
|
||||
|
||||
println!("PCG seed: 0x{:016x}", seed);
|
||||
println!("Running {} iterations...", difficulty);
|
||||
|
||||
let proof = solver::solve(&challenge, seed);
|
||||
|
||||
// 6. Output base64-encoded proof
|
||||
let proof_b64 = STANDARD.encode(&proof);
|
||||
println!("Proof size: {} bytes ({} digests)", proof.len(), proof.len() / 16);
|
||||
println!("n={}", proof_b64);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new().route("/solve", post(solve_handler));
|
||||
let addr = "0.0.0.0:3000";
|
||||
println!("hcaptcha-pow service listening on {addr}");
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user