All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tsec.jws.mac.JWSMacCV.scala Maven / Gradle / Ivy

The newest version!
package tsec.jws.mac

import java.security.MessageDigest
import java.time.Instant

import cats.MonadError
import cats.effect.Sync
import cats.implicits._
import tsec.common._
import tsec.jws._
import tsec.jwt.JWTClaims
import tsec.mac.{MessageAuth, _}
import tsec.mac.jca._

/** Our JWS Compressor, Signer and verifier (CV = Compressor and Verifier)
  *
  * @param hs our header serializer
  * @param programs our mac program implementation
  * @param M Monad instance for F
  * @tparam F Our effect type
  * @tparam A The mac signing algorithm
  */
sealed abstract class JWSMacCV[F[_], A](
    implicit hs: JWSSerializer[JWSMacHeader[A]],
    programs: MessageAuth[F, A, MacSigningKey],
    M: MonadError[F, Throwable]
) {

  /**  Generic Error. Any mishandling of the errors could leak information to an attacker. */
  private[this] def defaultError: MacError = MacVerificationError("Could not verify signature")

  private[this] def base64Safe(s: String): Either[Throwable, Array[Byte]] =
    s.b64UrlBytes match {
      case Some(b) => Right(b)
      case None    => Left(defaultError)
    }

  private[this] def base64SafeF(s: String): F[Array[Byte]] =
    s.b64UrlBytes match {
      case Some(b) => M.pure(b)
      case None    => M.raiseError(defaultError)
    }

  def sign(header: JWSMacHeader[A], body: JWTClaims, key: MacSigningKey[A]): F[MAC[A]] = {
    val toSign: String = hs.toB64URL(header) + "." + JWTClaims.toB64URL(body)
    programs.sign(toSign.asciiBytes, key)
  }

  def signAndBuild(header: JWSMacHeader[A], body: JWTClaims, key: MacSigningKey[A]): F[JWTMac[A]] = {
    val toSign: String = hs.toB64URL(header) + "." + JWTClaims.toB64URL(body)
    programs.sign(toSign.asciiBytes, key).map(s => JWTMac.buildToken[A](header, body, s))
  }

  def signToString(header: JWSMacHeader[A], body: JWTClaims, key: MacSigningKey[A]): F[String] = {
    val toSign: String = hs.toB64URL(header) + "." + JWTClaims.toB64URL(body)
    programs.sign(toSign.asciiBytes, key).map(s => toSign + "." + s.toB64UrlString)
  }

  def verifyBool(jwt: String, key: MacSigningKey[A], now: Instant): F[Boolean] = {
    val split: Array[String] = jwt.split("\\.", 3)
    if (split.length < 3)
      M.pure(false)
    else {
      split(2).b64UrlBytes match {
        case Some(providedBytes) =>
          (for {
            bytes  <- base64Safe(split(0))
            _      <- hs.fromUtf8Bytes(bytes)
            cBytes <- base64Safe(split(1))
            claims <- JWTClaims.fromUtf8Bytes(cBytes)
          } yield claims).fold(
            _ => M.pure(false),
            claims =>
              programs
                .sign((split(0) + "." + split(1)).asciiBytes, key)
                .map {
                  MessageDigest.isEqual(_, providedBytes) && claims.isNotExpired(now) && claims
                    .isAfterNBF(now) && claims.isValidIssued(now)
              }
          )
        case None =>
          M.pure(false)
      }
    }
  }

  def verify(jwt: String, key: MacSigningKey[A], now: Instant): F[VerificationStatus] =
    verifyBool(jwt, key, now).map(c => if (c) Verified else VerificationFailed)

  def verifyAndParse(jwt: String, key: MacSigningKey[A], now: Instant): F[JWTMac[A]] = {
    val split = jwt.split("\\.", 3)
    if (split.length != 3)
      M.raiseError(defaultError)
    else {
      split(2).b64UrlBytes match {
        case Some(signedBytes) =>
          for {
            hBytes <- base64SafeF(split(0))
            header <- M.fromEither(hs.fromUtf8Bytes(hBytes).left.map(_ => defaultError))
            claims <- M.fromEither(JWTClaims.fromB64URL(split(1)).left.map(_ => defaultError))
            bytes <- M.ensure(programs.sign((split(0) + "." + split(1)).asciiBytes, key))(defaultError)(
              signed =>
                MessageDigest.isEqual(signed, signedBytes)
                  && claims.isNotExpired(now)
                  && claims.isAfterNBF(now)
                  && claims.isValidIssued(now)
            )
          } yield JWTMac.buildToken[A](header, claims, bytes)
        case None => M.raiseError(defaultError)
      }
    }
  }

  def toEncodedString(jwt: JWTMac[A]): String =
    hs.toB64URL(jwt.header) + "." + JWTClaims.toB64URL(jwt.body) + "." + jwt.signature.toB64UrlString

  def parseUnverified(jwt: String): F[JWTMac[A]] = {
    val split = jwt.split("\\.", 3)
    if (split.length != 3)
      M.raiseError(defaultError)
    else {
      for {
        signedBytes <- base64SafeF(split(2))
        hBytes      <- base64SafeF(split(0))
        header      <- M.fromEither(hs.fromUtf8Bytes(hBytes).left.map(_ => defaultError))
        claims      <- M.fromEither(JWTClaims.fromB64URL(split(1)).left.map(_ => defaultError))
      } yield JWTMac.buildToken[A](header, claims, MAC[A](signedBytes))
    }
  }
}

object JWSMacCV {

  implicit def genSigner[F[_]: Sync, A](
      implicit hs: JWSSerializer[JWSMacHeader[A]],
      messageAuth: MessageAuth[F, A, MacSigningKey]
  ): JWSMacCV[F, A] =
    new JWSMacCV[F, A]() {}

  implicit def eitherSigner[A](
      implicit hs: JWSSerializer[JWSMacHeader[A]],
      messageAuth: MessageAuth[MacErrorM, A, MacSigningKey]
  ): JWSMacCV[MacErrorM, A] =
    new JWSMacCV[({type F[A] = Either[Throwable, A]})#F, A]() {}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy