///|
/// pkce.mbt — PKCE (Proof Key for Code Exchange) helpers per RFC 7636.
///
/// PKCE adds a layer of security to OAuth authorization code flow by
/// binding the authorization request to a verifier known only to the
/// client. Used by OpenAI's browser flow.
///|
/// A PKCE pair: the verifier is kept secret, the challenge is sent
/// in the authorization request.
pub(all) struct PkcePair {
code_verifier : String // 43-128 char random string
code_challenge : String // base64url(sha256(verifier))
code_challenge_method : String // always "S256"
}
///|
/// Generate a PKCE pair. The verifier is base64url of 48 random bytes
/// (64 chars, RFC 7636 §4.1 allows 43-128 unreserved characters).
pub fn generate_pkce() -> PkcePair {
let verifier = bytes_to_base64url(oauth_random_bytes(48))
let challenge = sha256_base64url(verifier)
{
code_verifier: verifier,
code_challenge: challenge,
code_challenge_method: "S256",
}
}
///|
/// Compute SHA-256 of a string and return base64url encoding (no padding).
fn sha256_base64url(input : String) -> String {
// @utf8.encode returns Bytes which implements ByteSource.
let bytes : Bytes = @utf8.encode(input)
let hash = @crypto.sha256(bytes)
// hash is FixedArray[Byte] (32 bytes); convert so one encoder serves both.
bytes_to_base64url(Bytes::makei(hash.length(), i => hash[i]))
}
///|
/// Base64url alphabet (RFC 4648 §5, no padding).
let b64url_chars : Array[Char] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
]
///|
/// Encode Bytes to a base64url string (no padding).
fn bytes_to_base64url(data : Bytes) -> String {
let buf = StringBuilder()
let len = data.length()
let mut i = 0
while i < len {
let b0 = data[i].to_int()
let b1 = if i + 1 < len { data[i + 1].to_int() } else { -1 }
let b2 = if i + 2 < len { data[i + 2].to_int() } else { -1 }
buf.write_char(b64url_chars[(b0 >> 2) & 0x3F])
buf.write_char(
b64url_chars[(((b0 & 0x03) << 4) | ((if b1 >= 0 { b1 } else { 0 }) >> 4)) &
0x3F],
)
if b1 >= 0 {
buf.write_char(
b64url_chars[(
((b1 & 0x0F) << 2) | ((if b2 >= 0 { b2 } else { 0 }) >> 6)
) &
0x3F],
)
}
if b2 >= 0 {
buf.write_char(b64url_chars[b2 & 0x3F])
}
i += 3
}
buf.to_string()
}