mirror of
https://github.com/fluencelabs/crypto
synced 2025-04-24 14:22:18 +00:00
Crypto jwt (#124)
* Introducing CryptoJWT * JWT logic is removed from Kademlia * Tiny fixes * CryptoJwt with dots * Fixed CryptoJwtSpec for scala.js
This commit is contained in:
parent
c27a1865c5
commit
38c6a466f1
@ -17,7 +17,14 @@
|
||||
|
||||
package fluence.crypto.signature
|
||||
|
||||
import fluence.crypto.{Crypto, KeyPair}
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.strong._
|
||||
import cats.syntax.compose._
|
||||
import fluence.crypto.{Crypto, CryptoError, KeyPair}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Signature algorithm -- cryptographically coupled keypair, signer and signature checker.
|
||||
@ -38,4 +45,28 @@ object SignAlgo {
|
||||
type SignerFn = KeyPair ⇒ Signer
|
||||
|
||||
type CheckerFn = KeyPair.Public ⇒ SignatureChecker
|
||||
|
||||
/**
|
||||
* Take checker, signature, and plain data, and apply checker, returning Unit on success, or left side error.
|
||||
*/
|
||||
private val fullChecker: Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] =
|
||||
new Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] {
|
||||
override def apply[F[_]: Monad](
|
||||
input: ((SignatureChecker, Signature), ByteVector)
|
||||
): EitherT[F, CryptoError, Unit] = {
|
||||
val ((signatureChecker, signature), plainData) = input
|
||||
signatureChecker.check(signature, plainData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For CheckerFn, builds a function that takes PubKeyAndSignature along with plain data, and checks the signature.
|
||||
*/
|
||||
def checkerFunc(fn: CheckerFn): Crypto.Func[(PubKeyAndSignature, ByteVector), Unit] =
|
||||
Crypto
|
||||
.liftFunc[PubKeyAndSignature, (SignatureChecker, Signature)] {
|
||||
case PubKeyAndSignature(pk, signature) ⇒ fn(pk) -> signature
|
||||
}
|
||||
.first[ByteVector] andThen fullChecker
|
||||
|
||||
}
|
||||
|
117
jwt/src/main/scala/fluence/crypto/jwt/CryptoJwt.scala
Normal file
117
jwt/src/main/scala/fluence/crypto/jwt/CryptoJwt.scala
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.jwt
|
||||
|
||||
import cats.syntax.compose._
|
||||
import cats.syntax.flatMap._
|
||||
import cats.syntax.functor._
|
||||
import fluence.codec.bits.BitsCodecs
|
||||
import fluence.codec.bits.BitsCodecs._
|
||||
import fluence.codec.{CodecError, PureCodec}
|
||||
import fluence.codec.circe.CirceCodecs
|
||||
import fluence.crypto.{Crypto, CryptoError, KeyPair}
|
||||
import fluence.crypto.signature.SignAlgo.CheckerFn
|
||||
import fluence.crypto.signature.{PubKeyAndSignature, SignAlgo, Signature, Signer}
|
||||
import io.circe.{Decoder, Encoder}
|
||||
import scodec.bits.{Bases, ByteVector}
|
||||
|
||||
/**
|
||||
* Primitivized version of JWT.
|
||||
*
|
||||
* @param readPubKey Gets public key from decoded Header and Claim
|
||||
* @tparam H Header type
|
||||
* @tparam C Claim type
|
||||
*/
|
||||
class CryptoJwt[H: Encoder: Decoder, C: Encoder: Decoder](
|
||||
readPubKey: PureCodec.Func[(H, C), KeyPair.Public]
|
||||
) {
|
||||
// Public Key reader, with errors lifted to Crypto
|
||||
private val readPK = Crypto.fromOtherFunc(readPubKey)(Crypto.liftCodecErrorToCrypto)
|
||||
|
||||
private val headerAndClaimCodec = Crypto.codec(CryptoJwt.headerClaimCodec[H, C])
|
||||
|
||||
private val signatureCodec = Crypto.codec(CryptoJwt.signatureCodec)
|
||||
|
||||
private val stringTripleCodec = Crypto.codec(CryptoJwt.stringTripleCodec)
|
||||
|
||||
// Take a JWT string, parse and deserialize it, check signature, return Header and Claim on success
|
||||
def reader(checkerFn: CheckerFn): Crypto.Func[String, (H, C)] =
|
||||
Crypto.liftFuncPoint[String, (H, C)](
|
||||
jwtToken ⇒
|
||||
for {
|
||||
triple ← stringTripleCodec.inverse.pointAt(jwtToken)
|
||||
(hc @ (h, c), s) = triple
|
||||
headerAndClaim ← headerAndClaimCodec.inverse.pointAt(hc)
|
||||
pk ← readPK.pointAt(headerAndClaim)
|
||||
signature ← signatureCodec.inverse.pointAt(s)
|
||||
plainData = ByteVector((h + c).getBytes())
|
||||
_ ← SignAlgo.checkerFunc(checkerFn).pointAt(PubKeyAndSignature(pk, signature) → plainData)
|
||||
} yield headerAndClaim
|
||||
)
|
||||
|
||||
// With the given Signer, serialize Header and Claim into JWT string, signing it on the way
|
||||
def writer(signer: Signer): Crypto.Func[(H, C), String] =
|
||||
Crypto.liftFuncPoint[(H, C), String](
|
||||
headerAndClaim ⇒
|
||||
for {
|
||||
pk ← readPK.pointAt(headerAndClaim)
|
||||
_ ← Crypto
|
||||
.liftFuncEither[Boolean, Unit](
|
||||
Either.cond(_, (), CryptoError("JWT encoded PublicKey doesn't match with signer's PublicKey"))
|
||||
)
|
||||
.pointAt(pk == signer.publicKey)
|
||||
hc ← headerAndClaimCodec.direct.pointAt(headerAndClaim)
|
||||
plainData = ByteVector((hc._1 + hc._2).getBytes())
|
||||
signature ← signer.sign.pointAt(plainData)
|
||||
s ← signatureCodec.direct.pointAt(signature)
|
||||
jwtToken ← stringTripleCodec.direct.pointAt((hc, s))
|
||||
} yield jwtToken
|
||||
)
|
||||
}
|
||||
|
||||
object CryptoJwt {
|
||||
|
||||
private val alphabet = Bases.Alphabets.Base64Url
|
||||
|
||||
private val strVec = BitsCodecs.base64AlphabetToVector(alphabet).swap
|
||||
|
||||
val signatureCodec: PureCodec[Signature, String] =
|
||||
PureCodec.liftB[Signature, ByteVector](_.sign, Signature(_)) andThen strVec
|
||||
|
||||
private def jsonCodec[T: Encoder: Decoder]: PureCodec[T, String] =
|
||||
CirceCodecs.circeJsonCodec[T] andThen
|
||||
CirceCodecs.circeJsonParseCodec andThen
|
||||
PureCodec.liftB[String, Array[Byte]](_.getBytes(), new String(_)) andThen
|
||||
PureCodec[Array[Byte], ByteVector] andThen
|
||||
strVec
|
||||
|
||||
val stringTripleCodec: PureCodec[((String, String), String), String] =
|
||||
PureCodec.liftEitherB(
|
||||
{
|
||||
case ((a, b), c) ⇒ Right(s"$a.$b.$c")
|
||||
},
|
||||
s ⇒
|
||||
s.split('.').toList match {
|
||||
case a :: b :: c :: Nil ⇒ Right(((a, b), c))
|
||||
case l ⇒ Left(CodecError("Wrong number of dot-divided parts, expected: 3, actual: " + l.size))
|
||||
}
|
||||
)
|
||||
|
||||
def headerClaimCodec[H: Encoder: Decoder, C: Encoder: Decoder]: PureCodec[(H, C), (String, String)] =
|
||||
jsonCodec[H] split jsonCodec[C]
|
||||
}
|
82
jwt/src/test/scala/fluence/crypto/jwt/CryptoJwtSpec.scala
Normal file
82
jwt/src/test/scala/fluence/crypto/jwt/CryptoJwtSpec.scala
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.jwt
|
||||
|
||||
import cats.{Id, Monad}
|
||||
import cats.data.EitherT
|
||||
import fluence.codec.PureCodec
|
||||
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
|
||||
import fluence.crypto.{Crypto, CryptoError, KeyPair}
|
||||
import io.circe.{Json, JsonNumber, JsonObject}
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
class CryptoJwtSpec extends WordSpec with Matchers {
|
||||
"CryptoJwt" should {
|
||||
val keys: Seq[KeyPair] =
|
||||
Stream.from(0).map(ByteVector.fromInt(_)).map(i ⇒ KeyPair.fromByteVectors(i, i))
|
||||
|
||||
val cryptoJwt =
|
||||
new CryptoJwt[JsonNumber, JsonObject](
|
||||
PureCodec.liftFunc(n ⇒ KeyPair.Public(ByteVector.fromInt(n._1.toInt.get)))
|
||||
)
|
||||
|
||||
implicit val checker: SignAlgo.CheckerFn =
|
||||
publicKey ⇒
|
||||
new SignatureChecker {
|
||||
override def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit] =
|
||||
EitherT.cond[F](signature.sign == plain.reverse, (), CryptoError("Signatures mismatch"))
|
||||
}
|
||||
|
||||
val signer: SignAlgo.SignerFn =
|
||||
keyPair ⇒ Signer(keyPair.publicKey, Crypto.liftFunc(plain ⇒ Signature(plain.reverse)))
|
||||
|
||||
"be a total bijection for valid JWT" in {
|
||||
val kp = keys.head
|
||||
val str = cryptoJwt
|
||||
.writer(signer(kp))
|
||||
.unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value. of test"))))
|
||||
|
||||
str.count(_ == '.') shouldBe 2
|
||||
|
||||
cryptoJwt.reader(checker).unsafe(str)._2.kleisli.run("test").get shouldBe Json.fromString("value. of test")
|
||||
}
|
||||
|
||||
"fail with wrong signer" in {
|
||||
val kp = keys.tail.head
|
||||
val str = cryptoJwt
|
||||
.writer(signer(kp))
|
||||
.runEither[Id](
|
||||
(JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value of test")))
|
||||
)
|
||||
|
||||
str.isLeft shouldBe true
|
||||
}
|
||||
|
||||
"fail with wrong signature" in {
|
||||
val kp = keys.head
|
||||
val str = cryptoJwt
|
||||
.writer(signer(kp))
|
||||
.unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value of test"))))
|
||||
|
||||
cryptoJwt.reader(checker).runEither[Id](str + "m").isLeft shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user