mirror of
https://github.com/fluencelabs/crypto
synced 2025-04-25 06:42:19 +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
|
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.
|
* Signature algorithm -- cryptographically coupled keypair, signer and signature checker.
|
||||||
@ -38,4 +45,28 @@ object SignAlgo {
|
|||||||
type SignerFn = KeyPair ⇒ Signer
|
type SignerFn = KeyPair ⇒ Signer
|
||||||
|
|
||||||
type CheckerFn = KeyPair.Public ⇒ SignatureChecker
|
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