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

// TLS 1.2's key derivation (RFC 5246 §5, §6.3, §8.1, RFC 7627): a PRF instead of HKDF, run over the
// hash of the negotiated suite.

///|
/// `P_hash(secret, seed)` (RFC 5246 §5): HMAC the seed again and again, each round folded back into
/// the next, out to `len` octets.
fn p_hash(
  secret : BytesView,
  start : BytesView,
  len : Int32,
  hasher : () -> &@spec.Hash,
) -> Bytes {
  let out = Buffer()
  let mut a = start.to_owned()
  while out.length() < len {
    a = @hmac.mac(secret, a[:], hasher)
    let block = Buffer()
    block.write_bytes(a)
    block.write_bytesview(start)
    out.write_bytes(@hmac.mac(secret, block.to_bytes()[:], hasher))
  }
  out.to_bytes()[0:len].to_owned()
}

///|
/// `PRF(secret, label, seed)` (RFC 5246 §5): `P_hash` over `label + seed`, out to `len` octets.
///
/// With `Md5Sha1` it is TLS 1.0's and 1.1's PRF instead (RFC 2246 §5): the secret is split in two
/// halves that overlap by an octet when its length is odd, `P_MD5` runs over the first and
/// `P_SHA1` over the second, and the two are XORed. That is a different construction, not a
/// different hash, which is why it is here and not in `Digest`.
pub fn prf(
  secret : BytesView,
  label : BytesView,
  seed : BytesView,
  len~ : Int32,
  digest~ : Digest = digest,
) -> Bytes {
  let tagged = Buffer()
  tagged.write_bytesview(label)
  tagged.write_bytesview(seed)
  let start = tagged.to_bytes()
  match digest {
    Md5Sha1 => {
      let half = (secret.length() + 1) / 2
      let first = p_hash(secret[0:half], start[:], len, fn() -> &@spec.Hash {
        @md5.Hasher::new()
      })
      let second = p_hash(secret[secret.length() - half:], start[:], len, fn() -> &@spec.Hash {
        @sha1.Hasher::new()
      })
      Bytes::makei(len, i => (first[i].to_int() ^ second[i].to_int()).to_byte())
    }
    _ => p_hash(secret, start[:], len, digest.hasher())
  }
}

///|
/// The master secret. `session` is the hash of the handshake through ClientKeyExchange when both
/// ends agreed extended_master_secret (RFC 7627 §4), which binds it to this handshake; `None`
/// falls back to the two hellos' randoms (RFC 5246 §8.1).
pub fn master12(
  premaster : BytesView,
  client_random : BytesView,
  server_random : BytesView,
  session? : BytesView,
  digest~ : Digest = digest,
) -> Bytes {
  match session {
    Some(hash) =>
      prf(premaster, b"extended master secret"[:], hash, len=48, digest~)
    None => {
      let seed = Buffer()
      seed.write_bytesview(client_random)
      seed.write_bytesview(server_random)
      prf(premaster, b"master secret"[:], seed.to_bytes()[:], len=48, digest~)
    }
  }
}

///|
/// The key block (RFC 5246 §6.3), which the suite cuts into keys and IVs. Note the seed's order:
/// the server's random first here, the client's first in the master secret.
pub fn key_block(
  master : BytesView,
  client_random : BytesView,
  server_random : BytesView,
  len~ : Int32,
  digest~ : Digest = digest,
) -> Bytes {
  let seed = Buffer()
  seed.write_bytesview(server_random)
  seed.write_bytesview(client_random)
  prf(master, b"key expansion"[:], seed.to_bytes()[:], len~, digest~)
}

///|
/// A TLS 1.2 exporter (RFC 5705 §4): `PRF(master_secret, label,
/// client_random + server_random + context)`. DTLS-SRTP over 1.2 exports its
/// keying material this way, under the same label the 1.3 path uses.
pub fn exporter12(
  master : BytesView,
  label : BytesView,
  client_random : BytesView,
  server_random : BytesView,
  context : BytesView,
  len~ : Int32,
  digest~ : Digest = digest,
) -> Bytes {
  let seed = Buffer()
  seed.write_bytesview(client_random)
  seed.write_bytesview(server_random)
  seed.write_bytesview(context)
  prf(master, label, seed.to_bytes()[:], len~, digest~)
}

///|
/// A Finished message's `verify_data` (RFC 5246 §7.4.9): twelve octets over the transcript hash,
/// `client` saying whose.
pub fn finished12(
  master : BytesView,
  transcript : BytesView,
  client~ : Bool,
  digest~ : Digest = digest,
) -> Bytes {
  let label = if client { b"client finished" } else { b"server finished" }
  prf(master, label[:], transcript, len=12, digest~)
}

///|
/// Whether a received Finished carries the `verify_data` expected, compared in constant time for
/// the reason `finished_ok` gives.
pub fn finished12_ok(
  master : BytesView,
  transcript : BytesView,
  verify_data : BytesView,
  client~ : Bool,
  digest~ : Digest = digest,
) -> Bool {
  @spec.eq(finished12(master, transcript, client~, digest~)[:], verify_data)
}