From c27a1865c5b088924455eab5329fe9d2a2847183 Mon Sep 17 00:00:00 2001 From: Dmitry Kurinskiy Date: Fri, 18 May 2018 18:24:53 +0300 Subject: [PATCH] crypto-keystore with codec and IO (#123) * crypto-keystore with codec and IO * Logger for readOrCreateKeyPair.readOrCreateKeyPair * Tests compilation fixed --- .../scala/fluence/crypto/SignatureSpec.scala | 6 +- .../crypto/keystore/FileKeyStorage.scala | 82 ++++++++----------- .../fluence/crypto/keystore/KeyStore.scala | 82 +++++++++---------- .../crypto/keystore/KeyStoreSpec.scala | 41 ++-------- 4 files changed, 82 insertions(+), 129 deletions(-) diff --git a/hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala b/hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala index 3e44536..52e5b0a 100644 --- a/hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala +++ b/hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala @@ -110,10 +110,10 @@ class SignatureSpec extends WordSpec with Matchers { if (keyFile.exists()) keyFile.delete() val storage = new FileKeyStorage(keyFile) - storage.storeSecretKey(keys) + storage.storeKeyPair(keys).unsafeRunSync() val keysReadE = storage.readKeyPair - val keysRead = keysReadE.get + val keysRead = keysReadE.unsafeRunSync() val signer = algo.signer(keys) val data = rndByteVector(10) @@ -123,7 +123,7 @@ class SignatureSpec extends WordSpec with Matchers { algo.checker(keysRead.publicKey).check(sign, data).isOk shouldBe true //try to store key into previously created file - storage.storeSecretKey(keys).isFailure shouldBe true + storage.storeKeyPair(keys).attempt.unsafeRunSync().isLeft shouldBe true } } } diff --git a/keystore/jvm/src/main/scala/fluence/crypto/keystore/FileKeyStorage.scala b/keystore/jvm/src/main/scala/fluence/crypto/keystore/FileKeyStorage.scala index 74f6317..3ff25d1 100644 --- a/keystore/jvm/src/main/scala/fluence/crypto/keystore/FileKeyStorage.scala +++ b/keystore/jvm/src/main/scala/fluence/crypto/keystore/FileKeyStorage.scala @@ -20,63 +20,50 @@ package fluence.crypto.keystore import java.io.File import java.nio.file.Files -import cats.MonadError -import cats.syntax.flatMap._ -import cats.syntax.functor._ +import cats.syntax.applicativeError._ +import cats.effect.IO +import fluence.codec.PureCodec import fluence.crypto.{signature, KeyPair} -import io.circe.parser.decode -import io.circe.syntax._ import scala.language.higherKinds +import scala.util.control.NonFatal /** - * TODO use cats IO * File based storage for crypto keys. * * @param file Path to keys in file system */ -class FileKeyStorage[F[_]](file: File)(implicit F: MonadError[F, Throwable]) extends slogging.LazyLogging { +class FileKeyStorage(file: File) extends slogging.LazyLogging { import KeyStore._ - def readKeyPair: F[KeyPair] = { - val keyBytes = Files.readAllBytes(file.toPath) // TODO: it throws! - for { - storageOp ← F.fromEither(decode[Option[KeyStore]](new String(keyBytes))) - storage ← storageOp match { - case None ⇒ - logger.warn(s"Reading keys from file=$file was failed") - F.raiseError[KeyStore](new RuntimeException("Cannot parse file with keys.")) - case Some(ks) ⇒ - logger.info(s"Reading keys from file=$file was success") - F.pure(ks) - } - } yield storage.keyPair + private val codec = PureCodec[KeyPair, String] + + private val readFile: IO[String] = + IO(Files.readAllBytes(file.toPath)).map(new String(_)) + + val readKeyPair: IO[KeyPair] = readFile.flatMap(codec.inverse.runF[IO]) + + private def writeFile(data: String): IO[Unit] = IO { + logger.info("Storing secret key to file: " + file) + if (!file.getParentFile.exists()) { + logger.info(s"Parent directory does not exist: ${file.getParentFile}, trying to create") + Files.createDirectories(file.getParentFile.toPath) + } + if (!file.exists()) file.createNewFile() else throw new RuntimeException(file.getAbsolutePath + " already exists") + Files.write(file.toPath, data.getBytes) } - def storeSecretKey(key: KeyPair): F[Unit] = - F.catchNonFatal { - logger.info("Storing secret key to file: " + file) - if (!file.getParentFile.exists()) { - logger.info(s"Parent directory does not exist: ${file.getParentFile}, trying to create") - Files.createDirectories(file.getParentFile.toPath) - } - if (!file.exists()) file.createNewFile() else throw new RuntimeException(file.getAbsolutePath + " already exists") - val str = KeyStore(key).asJson.toString() + def storeKeyPair(keyPair: KeyPair): IO[Unit] = + codec.direct.runF[IO](keyPair).flatMap(writeFile) - Files.write(file.toPath, str.getBytes) - } - - def getOrCreateKeyPair(f: ⇒ F[KeyPair]): F[KeyPair] = - if (file.exists()) { - readKeyPair - } else { - for { - newKeys ← f - _ ← storeSecretKey(newKeys) - } yield { - logger.info(s"New keys were generated and saved to file=$file") - newKeys - } + def readOrCreateKeyPair(createKey: IO[KeyPair]): IO[KeyPair] = + readKeyPair.recoverWith { + case NonFatal(e) ⇒ + logger.debug(s"KeyPair can't be loaded from $file, going to generate new keys", e) + for { + ks ← createKey + _ ← storeKeyPair(ks) + } yield ks } } @@ -89,9 +76,8 @@ object FileKeyStorage { * @param algo Sign algo * @return Keypair, either loaded or freshly generated */ - def getKeyPair[F[_]](keyPath: String, algo: signature.SignAlgo)(implicit F: MonadError[F, Throwable]): F[KeyPair] = { - val keyFile = new File(keyPath) - val keyStorage = new FileKeyStorage[F](keyFile) - keyStorage.getOrCreateKeyPair(algo.generateKeyPair.runF[F](None)) - } + def getKeyPair(keyPath: String, algo: signature.SignAlgo): IO[KeyPair] = + IO(new FileKeyStorage(new File(keyPath))) + .flatMap(_.readOrCreateKeyPair(algo.generateKeyPair.runF[IO](None))) + } diff --git a/keystore/src/main/scala/fluence/crypto/keystore/KeyStore.scala b/keystore/src/main/scala/fluence/crypto/keystore/KeyStore.scala index 83800aa..fc28c1c 100644 --- a/keystore/src/main/scala/fluence/crypto/keystore/KeyStore.scala +++ b/keystore/src/main/scala/fluence/crypto/keystore/KeyStore.scala @@ -17,17 +17,16 @@ package fluence.crypto.keystore -import cats.Monad -import cats.data.EitherT +import cats.syntax.compose._ +import fluence.codec.PureCodec +import fluence.codec.bits.BitsCodecs +import fluence.codec.circe.CirceCodecs import fluence.crypto.KeyPair -import io.circe.parser.decode -import io.circe.{Decoder, Encoder, HCursor, Json} +import io.circe.{HCursor, Json} import scodec.bits.{Bases, ByteVector} import scala.language.higherKinds -case class KeyStore(keyPair: KeyPair) - /** * Json example: * { @@ -47,44 +46,39 @@ object KeyStore { val Public = "public" } - implicit val encodeKeyStorage: Encoder[KeyStore] = (ks: KeyStore) ⇒ - Json.obj( - ( - Field.Keystore, - Json.obj( - (Field.Secret, Json.fromString(ks.keyPair.secretKey.value.toBase64(alphabet))), - (Field.Public, Json.fromString(ks.keyPair.publicKey.value.toBase64(alphabet))) - ) - ) - ) - - implicit val decodeKeyStorage: Decoder[Option[KeyStore]] = - (c: HCursor) ⇒ - for { - secret ← c.downField(Field.Keystore).downField(Field.Secret).as[String] - public ← c.downField(Field.Keystore).downField(Field.Public).as[String] - } yield { - for { - secret ← ByteVector.fromBase64(secret, alphabet) - public ← ByteVector.fromBase64(public, alphabet) - } yield KeyStore(KeyPair.fromByteVectors(public, secret)) - } - - def fromBase64[F[_]: Monad](base64: String): EitherT[F, IllegalArgumentException, KeyStore] = - for { - jsonStr ← ByteVector.fromBase64(base64, alphabet) match { - case Some(bv) ⇒ EitherT.pure[F, IllegalArgumentException](new String(bv.toArray)) - case None ⇒ - EitherT.leftT( - new IllegalArgumentException("'" + base64 + "' is not a valid base64.") + // Codec for a tuple of already serialized public and secret keys to json + private val pubSecJsonCodec: PureCodec[(String, String), Json] = + CirceCodecs.circeJsonCodec( + { + case (pub, sec) ⇒ + Json.obj( + ( + Field.Keystore, + Json.obj( + (Field.Secret, Json.fromString(sec)), + (Field.Public, Json.fromString(pub)) + ) + ) ) + }, + (c: HCursor) ⇒ + for { + sec ← c.downField(Field.Keystore).downField(Field.Secret).as[String] + pub ← c.downField(Field.Keystore).downField(Field.Public).as[String] + } yield (pub, sec) + ) + + // ByteVector to/from String, with the chosen alphabet + private val vecToStr = BitsCodecs.base64AlphabetToVector(alphabet).swap + + implicit val keyPairJsonCodec: PureCodec[KeyPair, Json] = + PureCodec.liftB[KeyPair, (ByteVector, ByteVector)]( + kp ⇒ (kp.publicKey.value, kp.secretKey.value), { + case (pub, sec) ⇒ KeyPair(KeyPair.Public(pub), KeyPair.Secret(sec)) } - keyStore ← decode[Option[KeyStore]](jsonStr) match { - case Right(Some(ks)) ⇒ EitherT.pure[F, IllegalArgumentException](ks) - case Right(None) ⇒ - EitherT.leftT[F, KeyStore](new IllegalArgumentException("'" + base64 + "' is not a valid key store.")) - case Left(err) ⇒ - EitherT.leftT[F, KeyStore](new IllegalArgumentException("'" + base64 + "' is not a valid key store.", err)) - } - } yield keyStore + ) andThen (vecToStr split vecToStr) andThen pubSecJsonCodec + + implicit val keyPairJsonStringCodec: PureCodec[KeyPair, String] = + keyPairJsonCodec andThen CirceCodecs.circeJsonParseCodec + } diff --git a/keystore/src/test/scala/fluence/crypto/keystore/KeyStoreSpec.scala b/keystore/src/test/scala/fluence/crypto/keystore/KeyStoreSpec.scala index 93d0dc2..af18f53 100644 --- a/keystore/src/test/scala/fluence/crypto/keystore/KeyStoreSpec.scala +++ b/keystore/src/test/scala/fluence/crypto/keystore/KeyStoreSpec.scala @@ -19,57 +19,30 @@ package fluence.crypto.keystore import fluence.crypto.KeyPair import org.scalatest.{Matchers, WordSpec} -import scodec.bits.{Bases, ByteVector} class KeyStoreSpec extends WordSpec with Matchers { - private val alphabet = Bases.Alphabets.Base64Url - - private val keyStore = KeyStore(KeyPair.fromBytes("pubKey".getBytes, "secKey".getBytes)) + private val keyPair = KeyPair.fromBytes("pubKey".getBytes, "secKey".getBytes) private val jsonString = """{"keystore":{"secret":"c2VjS2V5","public":"cHViS2V5"}}""" "KeyStore.encodeKeyStorage" should { "transform KeyStore to json" in { - val result = KeyStore.encodeKeyStorage(keyStore) - result.noSpaces shouldBe jsonString + val result = KeyStore.keyPairJsonStringCodec.direct.unsafe(keyPair) + result shouldBe jsonString } } "KeyStore.decodeKeyStorage" should { "transform KeyStore to json" in { - import io.circe.parser._ - val result = KeyStore.decodeKeyStorage.decodeJson(parse(jsonString).right.get) - result.right.get.get shouldBe keyStore + val result = KeyStore.keyPairJsonStringCodec.inverse.unsafe(jsonString) + result shouldBe keyPair } } "KeyStore" should { "transform KeyStore to json and back" in { - val result = KeyStore.decodeKeyStorage.decodeJson(KeyStore.encodeKeyStorage(keyStore)) - result.right.get.get shouldBe keyStore - } - } - - "fromBase64" should { - "throw an exception" when { - "invalid base64 format" in { - val invalidBase64 = "!@#$%" - - val e = KeyStore.fromBase64(invalidBase64).value.left.get - e should have message "'" + invalidBase64 + "' is not a valid base64." - } - "invalid decoded json" in { - val invalidJson = ByteVector("""{"keystore":{"public":"cHViS2V5"}}""".getBytes).toBase64(alphabet) - - KeyStore.fromBase64(invalidJson).value.isLeft shouldBe true - - } - } - - "fetch KeyStore from valid base64" in { - val invalidJson = ByteVector(jsonString.getBytes).toBase64(alphabet) - val result = KeyStore.fromBase64(invalidJson).value.right.get - result shouldBe keyStore + val result = KeyStore.keyPairJsonCodec.inverse.unsafe(KeyStore.keyPairJsonCodec.direct.unsafe(keyPair)) + result shouldBe keyPair } }