// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// caching_sha2_password — MySQL 8's default authentication plugin. Two phases:
//
//   * fast auth: the client sends SHA256(pw) XOR SHA256(SHA256(SHA256(pw)) ‖ nonce);
//     if the server has the account's SHA256 digest cached it replies "fast success".
//   * full auth (cache miss, always on the first connection to a fresh server):
//     lacking TLS, the client fetches the server's RSA public key and returns the
//     NUL-terminated password XOR-obfuscated with the nonce, RSA-OAEP encrypted.
//
// Both halves of that last step belong elsewhere and are taken from there: the
// PEM envelope and the SubjectPublicKeyInfo are `mooncred/x509`'s, RSA-OAEP is
// `mooncrypt/rsa`'s. What stays is the obfuscation, which is MySQL's own.

///|
/// The caching_sha2_password fast-auth scramble:
/// `SHA256(pw) XOR SHA256( SHA256(SHA256(pw)) ‖ nonce )`, 32 bytes. An empty
/// password sends an empty token.
pub fn caching_sha2_scramble(password : Bytes, nonce : Bytes) -> Bytes {
  if password.length() == 0 {
    return b""
  }
  let d1 = sha256(password)
  let inner = sha256(concat_bytes(sha256(d1), nonce))
  let out = Buffer()
  for i in 0.. @rsa.PublicKey raise MysqlError {
  @x509.Spki::read(@x509.pem(pem)[:]).rsa() catch {
    err =>
      raise ProtocolError("caching_sha2: bad public key — " + err.to_string())
  }
}

///|
/// The caching_sha2_password full-auth token: the NUL-terminated password XORed
/// with the nonce (cycled), then RSA-OAEP encrypted under the server's public key.
///
/// `seed` is the 20 random octets OAEP masks with. MySQL's own client draws them
/// from OpenSSL; here they are a parameter, because this package has no entropy
/// source and a caller that reuses a seed should have to write that down.
pub fn caching_sha2_full_auth_token(
  password : Bytes,
  nonce : Bytes,
  pem : String,
  seed : Bytes,
) -> Bytes raise MysqlError {
  guard nonce.length() > 0 else {
    raise ProtocolError("caching_sha2: empty server nonce")
  }
  let obf = Buffer()
  for i in 0..<(password.length() + 1) {
    let p = if i < password.length() { password[i].to_int() } else { 0 }
    obf.write_byte((p ^ nonce[i % nonce.length()].to_int()).to_byte())
  }
  // SHA-1 with an empty label is what OpenSSL's RSA_PKCS1_OAEP_PADDING does,
  // which is what the server expects — and what `encrypt` defaults to.
  parse_rsa_public_key(pem).encrypt(obf.to_bytes()[:], seed=seed[:]) catch {
    _ =>
      raise ProtocolError(
        "caching_sha2: password too long for RSA-OAEP under this key",
      )
  }
}