/// RADIUS password and authenticator helpers.

///|
fn hex_digit(value : Byte) -> Int {
  let n = value.to_int()
  if n >= 48 && n <= 57 {
    n - 48
  } else if n >= 97 && n <= 102 {
    n - 87
  } else {
    0
  }
}

///|
fn md5_hex_to_bytes(value : String) -> Bytes {
  let raw = @utf8.encode(value)
  Bytes::makei(raw.length() / 2, fn(index) {
    ((hex_digit(raw[index * 2]) << 4) | hex_digit(raw[index * 2 + 1])).to_byte()
  })
}

///|
fn md5_bytes(input : Bytes) -> Bytes {
  let digest = @md5.Digest::new()
  for byte in input {
    digest.write(byte)
  }
  md5_hex_to_bytes(digest.check_sum())
}

///|
fn password_block(secret : Bytes, previous : Bytes) -> Bytes {
  md5_bytes(join_bytes(secret, previous))
}

///|
/// Encrypt a User-Password attribute according to RFC 2865 section 5.2.
pub fn encrypt_user_password(
  password : Bytes,
  secret : Bytes,
  request_authenticator : Bytes,
) -> Bytes raise RadiusError {
  if request_authenticator.length() != 16 {
    raise InvalidAuthenticator
  }
  let padded_length = (password.length() + 15) / 16 * 16
  let padded = join_bytes(password, zeros(padded_length - password.length()))
  let mut result = b""
  let mut previous = request_authenticator
  for offset = 0; offset < padded.length(); offset = offset + 16 {
    let block = slice_bytes(padded, offset, 16)
    let mask = password_block(secret, previous)
    let encrypted = xor_bytes(block, mask)
    result = join_bytes(result, encrypted)
    previous = encrypted
  }
  if result.length() == 0 {
    zeros(16)
  } else {
    result
  }
}

///|
/// Decrypt a User-Password attribute according to RFC 2865 section 5.2.
pub fn decrypt_user_password(
  encrypted : Bytes,
  secret : Bytes,
  request_authenticator : Bytes,
) -> Bytes raise RadiusError {
  if encrypted.length() == 0 || encrypted.length() % 16 != 0 {
    raise InvalidAttribute(
      "encrypted password must be a non-empty multiple of 16 bytes",
    )
  }
  if request_authenticator.length() != 16 {
    raise InvalidAuthenticator
  }
  let mut result = b""
  let mut previous = request_authenticator
  for offset = 0; offset < encrypted.length(); offset = offset + 16 {
    let block = slice_bytes(encrypted, offset, 16)
    let mask = password_block(secret, previous)
    result = join_bytes(result, xor_bytes(block, mask))
    previous = block
  }
  result
}

///|
/// Compute the CHAP response value stored in CHAP-Password.
pub fn chap_response(
  identifier : Byte,
  password : Bytes,
  challenge : Bytes,
) -> Bytes {
  md5_bytes(
    join_bytes(
      join_bytes(Bytes::makei(1, fn(_) { identifier }), password),
      challenge,
    ),
  )
}

///|
/// Build the 17-byte CHAP-Password value (identifier followed by digest).
pub fn make_chap_password(
  identifier : Byte,
  password : Bytes,
  challenge : Bytes,
) -> Bytes {
  join_bytes(
    Bytes::makei(1, fn(_) { identifier }),
    chap_response(identifier, password, challenge),
  )
}

///|
/// Verify a CHAP-Password attribute against a cleartext shared secret.
pub fn verify_chap(
  attribute : Attribute,
  password : Bytes,
  challenge : Bytes,
) -> Bool {
  if attribute.kind != CHAP_Password || attribute.value.length() != 17 {
    false
  } else {
    let identifier = attribute.value[0]
    let expected = make_chap_password(identifier, password, challenge)
    byte_eq(attribute.value, expected)
  }
}

///|
/// Constant-time comparison for authenticators and password-derived values.
pub fn constant_time_equal(left : Bytes, right : Bytes) -> Bool {
  byte_eq(left, right)
}

///|
/// Return the decoded User-Password from a packet, if present.
pub fn Packet::user_password(
  self : Packet,
  secret : Bytes,
) -> Bytes? raise RadiusError {
  match self.attribute(User_Password) {
    Some(attribute) =>
      Some(
        decrypt_user_password(
          attribute.value,
          secret,
          self.header.authenticator,
        ),
      )
    None => None
  }
}