<commit_before>package org.http4s.server.middleware

import java.nio.charset.StandardCharsets
import java.security.{MessageDigest, SecureRandom}
import java.time.Clock
import java.util.Base64
import javax.crypto.spec.SecretKeySpec
import javax.crypto.{KeyGenerator, Mac, SecretKey}

import org.http4s.headers.{Cookie => HCookie}
import org.http4s.server.Middleware
import org.http4s.util.{CaseInsensitiveString, encodeHex}
import org.http4s._

import scalaz.concurrent.Task
import scalaz.syntax.std.boolean._
import scalaz.{Kleisli, OptionT}

final class CSRFMiddleware private[middleware] (
    val headerName: String = "X-Csrf-Token",
    val cookieName: String = "csrf-token",
    key: SecretKey,
    clock: Clock = Clock.systemUTC()) {

  /** Sign our token using the current time in milliseconds as a nonce
    * Signing and generating a token is potentially a unsafe operation
    * if constructed with a bad key.
    */
  private[middleware] def signToken(token: String): Task[String] = {
    val joined = token + "-" + clock.millis()
    Task.delay {
      val mac = Mac.getInstance(CSRFMiddleware.SigningAlgo)
      mac.init(key)
      val out = mac.doFinal(joined.getBytes(StandardCharsets.UTF_8))
      joined + "-" + Base64.getEncoder.encodeToString(out)
    }
  }

  /** Generate a new token **/
  private[middleware] def generateToken: Task[String] =
    signToken(CSRFMiddleware.genTokenString)

  /** Decode our CSRF token and extract the original token string to sign
    */
  private[middleware] def extractRaw(token: String): OptionT[Task, String] =
    token.split("-") match {
      case Array(raw, nonce, signed) =>
        OptionT[Task, String](Task.delay {
          val mac = Mac.getInstance(CSRFMiddleware.SigningAlgo)
          mac.init(key)
          val out =
            mac.doFinal((raw + "-" + nonce).getBytes(StandardCharsets.UTF_8))
          MessageDigest
            .isEqual(out, Base64.getDecoder.decode(signed))
            .option(raw)
        })
      case _ =>
        OptionT.none
    }

  /** Constructs a middleware that will check for the csrf token
    * presence on both the proper cookie, and header values.
    *
    * If it is a valid token, it will then embed a new one,
    * to effectively randomize the complete token while
    * avoiding the generation of a new secure random Id, to guard
    * against [BREACH](http://breachattack.com/)
    *
    */
  def validate: Middleware[Request, MaybeResponse, Request, MaybeResponse] = {
    service =>
      Kleisli[Task, Request, MaybeResponse] { r =>
        (for {
          c1 <- CSRFMiddleware.cookieFromHeaders(r, cookieName)
          c2 <- OptionT(
            Task.point(r.headers.get(CaseInsensitiveString(headerName))))
          raw1 <- extractRaw(c1.content)
          raw2 <- extractRaw(c2.value)
          response <- if (CSRFMiddleware.isEqual(raw1, raw2)) {
            OptionT[Task, MaybeResponse](service(r).map(Some(_)))
          } else {
            OptionT.none[Task, MaybeResponse]
          }
          newToken <- OptionT[Task, String](signToken(raw1).map(Some(_)))
        } yield
          response.cata(_.addCookie(Cookie(cookieName, newToken)),
                        Response(Status.NotFound)))
          .getOrElse(Response(Status.Unauthorized))
      }
  }

  /** Embed a token into a response **/
  def embedNew(res: MaybeResponse): Task[Response] =
    generateToken.map(
      content =>
        res.cata(_.addCookie(Cookie(cookieName, content)),
                 Response(Status.NotFound)))

  /** Middleware to embed a csrf token into routes that do not have one. **/
  def withNewToken: Middleware[Request, MaybeResponse, Request, MaybeResponse] =
    _.andThen(Kleisli(embedNew))

}

object CSRFMiddleware {

  /** Default method for constructing CSRF middleware **/
  def apply(headerName: String = "X-Csrf-Token",
            cookieName: String = "csrf-token",
            key: SecretKey,
            clock: Clock = Clock.systemUTC()): CSRFMiddleware =
    new CSRFMiddleware(headerName, cookieName, key, clock)

  /** Sugar for instantiating a middleware by generating a key **/
  def withGeneratedKey(headerName: String = "X-Csrf-Token",
                       cookieName: String = "csrf-token",
                       clock: Clock = Clock.systemUTC()): Task[CSRFMiddleware] =
    generateSigningKey().map(apply(headerName, cookieName, _, clock))

  /** Sugar for pre-loading a key **/
  def withKeyBytes(keyBytes: Array[Byte],
                   headerName: String = "X-Csrf-Token",
                   cookieName: String = "csrf-token",
                   clock: Clock = Clock.systemUTC()): Task[CSRFMiddleware] =
    buildSigningKey(keyBytes).map(apply(headerName, cookieName, _, clock))

  /** An instance of SecureRandom to generate
    * tokens, properly seeded:
    * https://tersesystems.com/blog/2015/12/17/the-right-way-to-use-securerandom/
    */
  private val CachedRandom = {
    val r = new SecureRandom()
    r.nextBytes(new Array[Byte](20))
    r
  }

  val SigningAlgo = "HmacSHA1"
  val SHA1ByteLen = 20
  val CSRFTokenLength = 32

  private[middleware] def cookieFromHeaders(
      request: Request,
      headerName: String): OptionT[Task, Cookie] =
    OptionT(
      Task.point(
        HCookie
          .from(request.headers)
          .flatMap(_.values.find(_.name == headerName))))

  /** A Constant-time string equality **/
  def isEqual(s1: String, s2: String): Boolean =
    MessageDigest.isEqual(s1.getBytes(StandardCharsets.UTF_8),
                          s2.getBytes(StandardCharsets.UTF_8))

  /** Generate an unsigned CSRF token from a `SecureRandom` **/
  private[middleware] def genTokenString: String = {
    val bytes = new Array[Byte](CSRFTokenLength)
    CachedRandom.nextBytes(bytes)
    new String(encodeHex(bytes))
  }

  /** Generate a signing Key for the CSRF token **/
  def generateSigningKey(): Task[SecretKey] =
    Task.delay(KeyGenerator.getInstance(SigningAlgo).generateKey())

  /** Build a new HMACSHA1 Key for our CSRF Middleware
    * from key bytes. This operation is unsafe, in that
    * any amount less than 20 bytes will throw an exception when loaded<commit_msg>fix csrf for backwards compat<commit_after>package org.http4s.server.middleware

import java.nio.charset.StandardCharsets
import java.security.{MessageDigest, SecureRandom}
import java.time.Clock
import java.util.Base64
import javax.crypto.spec.SecretKeySpec
import javax.crypto.{KeyGenerator, Mac, SecretKey}

import org.http4s.headers.{Cookie => HCookie}
import org.http4s.server.Middleware
import org.http4s.util.{CaseInsensitiveString, encodeHex}
import org.http4s._

import scalaz.concurrent.Task
import scalaz.syntax.std.boolean._
import scalaz.{Kleisli, OptionT}

final class CSRFMiddleware private[middleware] (
    val headerName: String = "X-Csrf-Token",
    val cookieName: String = "csrf-token",
    key: SecretKey,
    clock: Clock = Clock.systemUTC()) {

  /** Sign our token using the current time in milliseconds as a nonce
    * Signing and generating a token is potentially a unsafe operation
    * if constructed with a bad key.
    */
  private[middleware] def signToken(token: String): Task[String] = {
    val joined = token + "-" + clock.millis()
    Task.delay {
      val mac = Mac.getInstance(CSRFMiddleware.SigningAlgo)
      mac.init(key)
      val out = mac.doFinal(joined.getBytes(StandardCharsets.UTF_8))
      joined + "-" + Base64.getEncoder.encodeToString(out)
    }
  }

  /** Generate a new token **/
  private[middleware] def generateToken: Task[String] =
    signToken(CSRFMiddleware.genTokenString)

  /** Decode our CSRF token and extract the original token string to sign
    */
  private[middleware] def extractRaw(token: String): OptionT[Task, String] =
    token.split("-") match {
      case Array(raw, nonce, signed) =>
        OptionT[Task, String](Task.delay {
          val mac = Mac.getInstance(CSRFMiddleware.SigningAlgo)
          mac.init(key)
          val out =
            mac.doFinal((raw + "-" + nonce).getBytes(StandardCharsets.UTF_8))
          MessageDigest
            .isEqual(out, Base64.getDecoder.decode(signed))
            .option(raw)
        })
      case _ =>
        OptionT.none
    }

  /** Constructs a middleware that will check for the csrf token
    * presence on both the proper cookie, and header values.
    *
    * If it is a valid token, it will then embed a new one,
    * to effectively randomize the complete token while
    * avoiding the generation of a new secure random Id, to guard
    * against [BREACH](http://breachattack.com/)
    *
    */
  def validate: Middleware[Request, MaybeResponse, Request, MaybeResponse] = {
    service =>
      Kleisli[Task, Request, MaybeResponse] { r =>
        (for {
          c1 <- CSRFMiddleware.cookieFromHeaders(r, cookieName)
          c2 <- OptionT(
            Task.now(r.headers.get(CaseInsensitiveString(headerName))))
          raw1 <- extractRaw(c1.content)
          raw2 <- extractRaw(c2.value)
          response <- if (CSRFMiddleware.isEqual(raw1, raw2)) {
            OptionT[Task, MaybeResponse](service(r).map(Some(_)))
          } else {
            OptionT.none[Task, MaybeResponse]
          }
          newToken <- OptionT[Task, String](signToken(raw1).map(Some(_)))
        } yield
          response.cata(_.addCookie(Cookie(cookieName, newToken)),
                        Response(Status.NotFound)))
          .getOrElse(Response(Status.Unauthorized))
      }
  }

  /** Embed a token into a response **/
  def embedNew(res: MaybeResponse): Task[Response] =
    generateToken.map(
      content =>
        res.cata(_.addCookie(Cookie(cookieName, content)),
                 Response(Status.NotFound)))

  /** Middleware to embed a csrf token into routes that do not have one. **/
  def withNewToken: Middleware[Request, MaybeResponse, Request, MaybeResponse] =
    _.andThen(Kleisli(embedNew))

}

object CSRFMiddleware {

  /** Default method for constructing CSRF middleware **/
  def apply(headerName: String = "X-Csrf-Token",
            cookieName: String = "csrf-token",
            key: SecretKey,
            clock: Clock = Clock.systemUTC()): CSRFMiddleware =
    new CSRFMiddleware(headerName, cookieName, key, clock)

  /** Sugar for instantiating a middleware by generating a key **/
  def withGeneratedKey(headerName: String = "X-Csrf-Token",
                       cookieName: String = "csrf-token",
                       clock: Clock = Clock.systemUTC()): Task[CSRFMiddleware] =
    generateSigningKey().map(apply(headerName, cookieName, _, clock))

  /** Sugar for pre-loading a key **/
  def withKeyBytes(keyBytes: Array[Byte],
                   headerName: String = "X-Csrf-Token",
                   cookieName: String = "csrf-token",
                   clock: Clock = Clock.systemUTC()): Task[CSRFMiddleware] =
    buildSigningKey(keyBytes).map(apply(headerName, cookieName, _, clock))

  /** An instance of SecureRandom to generate
    * tokens, properly seeded:
    * https://tersesystems.com/blog/2015/12/17/the-right-way-to-use-securerandom/
    */
  private val CachedRandom = {
    val r = new SecureRandom()
    r.nextBytes(new Array[Byte](20))
    r
  }

  val SigningAlgo = "HmacSHA1"
  val SHA1ByteLen = 20
  val CSRFTokenLength = 32

  private[middleware] def cookieFromHeaders(
      request: Request,
      headerName: String): OptionT[Task, Cookie] =
    OptionT(
      Task.now(
        HCookie
          .from(request.headers)
          .flatMap(_.values.find(_.name == headerName))))

  /** A Constant-time string equality **/
  def isEqual(s1: String, s2: String): Boolean =
    MessageDigest.isEqual(s1.getBytes(StandardCharsets.UTF_8),
                          s2.getBytes(StandardCharsets.UTF_8))

  /** Generate an unsigned CSRF token from a `SecureRandom` **/
  private[middleware] def genTokenString: String = {
    val bytes = new Array[Byte](CSRFTokenLength)
    CachedRandom.nextBytes(bytes)
    new String(encodeHex(bytes))
  }

  /** Generate a signing Key for the CSRF token **/
  def generateSigningKey(): Task[SecretKey] =
    Task.delay(KeyGenerator.getInstance(SigningAlgo).generateKey())

  /** Build a new HMACSHA1 Key for our CSRF Middleware
    * from key bytes. This operation is unsafe, in that
    * any amount less than 20 bytes will throw an exception when loaded
{'fraction_non_alphanumeric': 0.11157894736842106, 'fraction_numerical': 0.007611336032388664, 'mean_word_length': 2.885184020132117, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 15, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6542602', 'n_tokens_mistral': 3760, 'n_tokens_neox': 3668, 'n_words': 923}