use super::funejson::Value; use super::auth_jwt_decode_jwt::check_jwt_secret; use super::crypto_hmac_sha256::hmac_sha256; use super::encoding_base64_base64_url_encode::base64_url_encode; /// base64url of {"alg":"HS256","typ":"JWT"}: the header is fixed. const HEADER: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; const MAX_DEPTH: usize = 32; const MAX_SAFE: i64 = 9007199254740991; fn quote(text: &str, out: &mut String) { out.push('"'); for ch in text.chars() { match ch { '"' => out.push_str("\\\""), '\\' => out.push_str("\\\\"), '\n' => out.push_str("\\n"), '\r' => out.push_str("\\r"), '\t' => out.push_str("\\t"), '\u{8}' => out.push_str("\\b"), '\u{c}' => out.push_str("\\f"), c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)), c => out.push(c), } } out.push('"'); } /// One canonical JSON text: no spaces, object keys sorted by code point /// (Rust's `String` order), whole numbers only, non-ASCII written as UTF-8. fn canonical(value: &Value, depth: usize, out: &mut String) { const WHOLE: &str = "claims may only hold whole numbers from -(2^53 - 1) to 2^53 - 1"; match value { Value::Null => out.push_str("null"), Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }), Value::Int(i) => { if i.abs() > MAX_SAFE { panic!("{}", WHOLE); } out.push_str(&i.to_string()); } Value::Float(f) => { if !f.is_finite() || f.fract() != 0.0 || f.abs() > MAX_SAFE as f64 { panic!("{}", WHOLE); } out.push_str(&(*f as i64).to_string()); } Value::Str(s) => quote(s, out), Value::Arr(items) => { if depth > MAX_DEPTH { panic!("claims are nested more than 32 deep"); } out.push('['); for (i, item) in items.iter().enumerate() { if i > 0 { out.push(','); } canonical(item, depth + 1, out); } out.push(']'); } Value::Obj(pairs) => { if depth > MAX_DEPTH { panic!("claims are nested more than 32 deep"); } let mut sorted: Vec<&(String, Value)> = pairs.iter().collect(); sorted.sort_by(|a, b| a.0.cmp(&b.0)); out.push('{'); for (i, (key, item)) in sorted.into_iter().enumerate() { if i > 0 { out.push(','); } quote(key, out); out.push(':'); canonical(item, depth + 1, out); } out.push('}'); } } } /// An HS256 JWT for the claims: header {"alg":"HS256","typ":"JWT"}, the /// claims as canonical JSON, and the HMAC-SHA256 of the two under the secret. /// /// # Panics /// Panics if the secret is under 32 bytes, the claims are not an object, or /// they hold a fraction, an unsafe integer or nesting past 32. pub fn sign_jwt(claims: &Value, secret: &[i64]) -> String { check_jwt_secret(secret); if !matches!(claims, Value::Obj(_)) { panic!("claims must be a JSON object"); } let mut payload = String::new(); canonical(claims, 1, &mut payload); let payload_bytes: Vec = payload.bytes().map(|b| b as i64).collect(); let signing_input = format!("{}.{}", HEADER, base64_url_encode(&payload_bytes)); let input: Vec = signing_input.bytes().map(|b| b as i64).collect(); format!("{}.{}", signing_input, base64_url_encode(&hmac_sha256(secret, &input))) } pub fn fune_vector(args: &[Value]) -> Value { let secret: Vec = match &args[1] { Value::Arr(items) => items .iter() .map(|item| match item { Value::Int(i) => *i, _ => panic!("secret must be a list of integers from 0 to 255"), }) .collect(), _ => panic!("secret must be a list of integers from 0 to 255"), }; Value::str(&sign_jwt(&args[0], &secret)) }