///| SCRAM-SHA-256 client, specialised to what KingbaseES V9R1C010 accepts.

///|

///| KingbaseES follows RFC 5802 with two quirks verified against the live

///| server: the client-first-message keeps its `n,,` GS2 header and uses the

///| anonymous username `n=*` (the role itself comes from the startup packet),

///| and the AuthMessage is computed over the *bare* message, i.e. with the GS2

///|
/// header removed. `c=biws` is base64 of `n,,`, meaning "no channel binding".
priv struct ScramState {
  client_first : String
  client_nonce : String
}

///| SASLInitialResponse payload: mechanism name, NUL, the byte length of the

///|
/// client-first-message, then the message itself.
fn scram_initial_response(cf : String) -> Bytes {
  let w = @wire.new_writer(24 + cf.length())
  w.text("SCRAM-SHA-256")
  w.byte(b'\x00')
  w.int32(cf.length())
  w.text(cf)
  w.payload()
}

///|
/// Client-first-message plus the nonce it embeds.
fn scram_client_first(nonce_bytes : Int) -> ScramState {
  let cnonce = @crypto.base64_encode(@sys.random_bytes(nonce_bytes))
  let cf = "n,,n=*,r=\{cnonce}"
  { client_first: cf, client_nonce: cnonce, }
}

///|
/// Value of a single-letter SCRAM attribute (`r=`, `s=`, `i=`).
fn scram_attr(server_msg : String, key : String) -> String {
  for part in server_msg.split(",") {
    if part.has_prefix(key) {
      return part[key.length():].to_owned()
    }
  }
  ""
}

///|
priv struct ScramReply {
  client_final : String
  server_signature : String
}

///|
/// Computes the client-final message and the signature the server must return.
fn scram_respond(
  state : ScramState,
  server_first : String,
  password : String,
) -> Result[ScramReply, String] {
  let server_nonce = scram_attr(server_first, "r=")
  let salt_b64 = scram_attr(server_first, "s=")
  let iters_str = scram_attr(server_first, "i=")
  if !server_nonce.has_prefix(state.client_nonce) {
    return Err("nonce mismatch")
  }
  let salt = match @crypto.base64_decode(salt_b64) {
    Ok(b) => b
    Err(e) => return Err("bad salt: \{e}")
  }
  let parsed : Result[Int, String] = Ok(@string.parse_int(iters_str)) catch {
    _ => Err("bad iteration count '\{iters_str}'")
  }
  let iters = match parsed {
    Ok(n) if n > 0 => n
    Ok(_) => return Err("iteration count must be positive")
    Err(msg) => return Err(msg)
  }
  let without_proof = "c=biws,r=\{server_nonce}"
  // RFC 5802: the AuthMessage uses the bare client-first-message, so the
  // 3-byte GS2 header is dropped here.
  let bare_first = state.client_first[3:]
  let auth_message = @encoding/utf8.encode(
    "\{bare_first},\{server_first},\{without_proof}",
  )
  let salted = @crypto.pbkdf2_hmac_sha256(
    @encoding/utf8.encode(password),
    salt,
    iters,
    32,
  )
  let client_key = @crypto.hmac_sha256(salted, b"Client Key")
  let stored_key = @crypto.sha256(client_key)
  let client_sig = @crypto.hmac_sha256(stored_key, auth_message)
  let proof_buf = Buffer()
  let mut i = 0
  while i < 32 {
    let x = (client_key[i].to_int() ^ client_sig[i].to_int()).to_byte()
    proof_buf.write_byte(x)
    i = i + 1
  }
  let proof = proof_buf.to_bytes()
  let server_key = @crypto.hmac_sha256(salted, b"Server Key")
  let server_sig = @crypto.hmac_sha256(server_key, auth_message)
  let final_msg = "\{without_proof},p=\{@crypto.base64_encode(proof)}"
  let sig_text = @crypto.base64_encode(server_sig)
  Ok({ client_final: final_msg, server_signature: sig_text, })
}

///|
/// The SASLFinal payload the server must echo, proving the same password.
fn scram_expected_server_final(reply : ScramReply) -> String {
  "v=\{reply.server_signature}"
}