KeiSeiKit-1.0/_primitives/_rust/kei-auth-google/src/pkce.rs
Parfii-bot f47c087646 feat(auth): JWT verification + OAuth CSRF + PKCE + secret redaction
Group B — auth-crate security hardening (post-audit Sonnet test-retest 2026-05-02).

kei-auth-apple:
- jwt.rs: full ES256 JWKS signature verification (jsonwebtoken crate);
          validates iss == https://appleid.apple.com, aud == client_id, exp, iat;
          decode_id_token_unverified is now cfg(test)-only.
          Module docstring promised this since v0.1 — now actually implemented.
- claims.rs (new): IdTokenClaims + AudClaim extracted from jwt.rs.
- error.rs: JwtVerify, JwtDecode, MissingClaim variants.
- client.rs: client_secret_jwt: SecretString (was String); exchange_code accepts
              code_verifier: Option<&str> for PKCE.
- provider.rs: verify() does CSRF expected_state ConstantTimeEq + JWT verification;
                build_auth_url accepts state + verifier and emits PKCE code_challenge.
- tests/apple_smoke.rs + helpers/: 6 tests including malformed-JWT + non-Apple OAuth +
                                     400-mapping + provider_verify_csrf_mismatch_rejected.

kei-auth-google:
- pkce.rs (new): pkce_challenge + url_encode (RFC 7636 §B.1 test vector covered).
- client.rs: client_secret: SecretString; exchange_code accepts code_verifier.
- provider.rs: verify() rejects on state mismatch; build_auth_url emits S256 challenge.
- tests/google_smoke.rs: 7 tests including CSRF mismatch.

kei-auth:
- main.rs: resolve_token() supports stdin (-) and KEI_AUTH_TOKEN env. Token positional
            arg leaked via /proc/<pid>/cmdline + shell history; same fix that v0.14.1
            applied to --key.
- main.rs::key(): hard fail if KEI_AUTH_KEY len < 32 bytes (mirror of magiclink).
- tokens.rs::verify(): query_row(...).optional()? instead of .ok() — DB errors now
                        propagate instead of being swallowed as "token unknown".

kei-runtime-core:
- secrets.rs (new, 81 LOC): SecretString newtype with redacted Debug + zeroize-on-Drop.
                              Required by every auth crate that holds secret material.
- traits/auth.rs: AuthChallenge::Password.password is now SecretString;
                   OAuthCode { state, expected_state }.
- error.rs: CsrfStateMismatch variant.

Test results: 48 passed; 0 failed across kei-auth, kei-auth-apple, kei-auth-google,
kei-auth-magiclink, kei-runtime-core. cargo check --workspace clean.

Findings consensus: Apple JWT unverified + OAuth state CSRF appeared in all 3
audit waves (Wave-1 + Wave-A + Wave-B); PKCE absence + secret-derive-Debug appeared
only in Wave-A retest, would have been missed by single-pass audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 21:39:18 +08:00

51 lines
1.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 <author org>
//!
//! PKCE (RFC 7636) helpers and URL percent-encoder shared by the Google
//! auth provider.
use base64::Engine as _;
use sha2::{Digest, Sha256};
/// Compute PKCE `code_challenge` = `BASE64URL-no-pad(SHA256(verifier))`.
///
/// The `code_verifier` is a high-entropy random string (ASCII unreserved
/// characters, 43128 chars). See RFC 7636 §4.1.
pub fn pkce_challenge(verifier: &str) -> String {
let hash = Sha256::digest(verifier.as_bytes());
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
}
/// Percent-encode a string per RFC 3986 §2.1 (only unreserved chars pass).
pub(crate) fn url_encode(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for b in input.bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
out.push(b as char);
} else {
out.push_str(&format!("%{b:02X}"));
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn url_encode_basics() {
assert_eq!(url_encode("a b"), "a%20b");
assert_eq!(url_encode("openid email profile"), "openid%20email%20profile");
assert_eq!(url_encode("https://x/cb"), "https%3A%2F%2Fx%2Fcb");
assert_eq!(url_encode("safe-_.~"), "safe-_.~");
}
#[test]
fn pkce_challenge_is_base64url_sha256() {
// RFC 7636 §B.1 test vector.
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let challenge = pkce_challenge(verifier);
assert_eq!(challenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM");
}
}