crypto-keystore with codec and IO (#123)

* crypto-keystore with codec and IO

* Logger for readOrCreateKeyPair.readOrCreateKeyPair

* Tests compilation fixed
This commit is contained in:
Dmitry Kurinskiy 2018-05-18 18:24:53 +03:00 committed by GitHub
parent 88034b371f
commit c27a1865c5
4 changed files with 82 additions and 129 deletions

View File

@ -110,10 +110,10 @@ class SignatureSpec extends WordSpec with Matchers {
if (keyFile.exists()) keyFile.delete() if (keyFile.exists()) keyFile.delete()
val storage = new FileKeyStorage(keyFile) val storage = new FileKeyStorage(keyFile)
storage.storeSecretKey(keys) storage.storeKeyPair(keys).unsafeRunSync()
val keysReadE = storage.readKeyPair val keysReadE = storage.readKeyPair
val keysRead = keysReadE.get val keysRead = keysReadE.unsafeRunSync()
val signer = algo.signer(keys) val signer = algo.signer(keys)
val data = rndByteVector(10) val data = rndByteVector(10)
@ -123,7 +123,7 @@ class SignatureSpec extends WordSpec with Matchers {
algo.checker(keysRead.publicKey).check(sign, data).isOk shouldBe true algo.checker(keysRead.publicKey).check(sign, data).isOk shouldBe true
//try to store key into previously created file //try to store key into previously created file
storage.storeSecretKey(keys).isFailure shouldBe true storage.storeKeyPair(keys).attempt.unsafeRunSync().isLeft shouldBe true
} }
} }
} }

View File

@ -20,63 +20,50 @@ package fluence.crypto.keystore
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import cats.MonadError import cats.syntax.applicativeError._
import cats.syntax.flatMap._ import cats.effect.IO
import cats.syntax.functor._ import fluence.codec.PureCodec
import fluence.crypto.{signature, KeyPair} import fluence.crypto.{signature, KeyPair}
import io.circe.parser.decode
import io.circe.syntax._
import scala.language.higherKinds import scala.language.higherKinds
import scala.util.control.NonFatal
/** /**
* TODO use cats IO
* File based storage for crypto keys. * File based storage for crypto keys.
* *
* @param file Path to keys in file system * @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._ import KeyStore._
def readKeyPair: F[KeyPair] = { private val codec = PureCodec[KeyPair, String]
val keyBytes = Files.readAllBytes(file.toPath) // TODO: it throws!
for { private val readFile: IO[String] =
storageOp F.fromEither(decode[Option[KeyStore]](new String(keyBytes))) IO(Files.readAllBytes(file.toPath)).map(new String(_))
storage storageOp match {
case None val readKeyPair: IO[KeyPair] = readFile.flatMap(codec.inverse.runF[IO])
logger.warn(s"Reading keys from file=$file was failed")
F.raiseError[KeyStore](new RuntimeException("Cannot parse file with keys.")) private def writeFile(data: String): IO[Unit] = IO {
case Some(ks) logger.info("Storing secret key to file: " + file)
logger.info(s"Reading keys from file=$file was success") if (!file.getParentFile.exists()) {
F.pure(ks) logger.info(s"Parent directory does not exist: ${file.getParentFile}, trying to create")
} Files.createDirectories(file.getParentFile.toPath)
} yield storage.keyPair }
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] = def storeKeyPair(keyPair: KeyPair): IO[Unit] =
F.catchNonFatal { codec.direct.runF[IO](keyPair).flatMap(writeFile)
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()
Files.write(file.toPath, str.getBytes) def readOrCreateKeyPair(createKey: IO[KeyPair]): IO[KeyPair] =
} readKeyPair.recoverWith {
case NonFatal(e)
def getOrCreateKeyPair(f: F[KeyPair]): F[KeyPair] = logger.debug(s"KeyPair can't be loaded from $file, going to generate new keys", e)
if (file.exists()) { for {
readKeyPair ks createKey
} else { _ storeKeyPair(ks)
for { } yield ks
newKeys f
_ storeSecretKey(newKeys)
} yield {
logger.info(s"New keys were generated and saved to file=$file")
newKeys
}
} }
} }
@ -89,9 +76,8 @@ object FileKeyStorage {
* @param algo Sign algo * @param algo Sign algo
* @return Keypair, either loaded or freshly generated * @return Keypair, either loaded or freshly generated
*/ */
def getKeyPair[F[_]](keyPath: String, algo: signature.SignAlgo)(implicit F: MonadError[F, Throwable]): F[KeyPair] = { def getKeyPair(keyPath: String, algo: signature.SignAlgo): IO[KeyPair] =
val keyFile = new File(keyPath) IO(new FileKeyStorage(new File(keyPath)))
val keyStorage = new FileKeyStorage[F](keyFile) .flatMap(_.readOrCreateKeyPair(algo.generateKeyPair.runF[IO](None)))
keyStorage.getOrCreateKeyPair(algo.generateKeyPair.runF[F](None))
}
} }

View File

@ -17,17 +17,16 @@
package fluence.crypto.keystore package fluence.crypto.keystore
import cats.Monad import cats.syntax.compose._
import cats.data.EitherT import fluence.codec.PureCodec
import fluence.codec.bits.BitsCodecs
import fluence.codec.circe.CirceCodecs
import fluence.crypto.KeyPair import fluence.crypto.KeyPair
import io.circe.parser.decode import io.circe.{HCursor, Json}
import io.circe.{Decoder, Encoder, HCursor, Json}
import scodec.bits.{Bases, ByteVector} import scodec.bits.{Bases, ByteVector}
import scala.language.higherKinds import scala.language.higherKinds
case class KeyStore(keyPair: KeyPair)
/** /**
* Json example: * Json example:
* { * {
@ -47,44 +46,39 @@ object KeyStore {
val Public = "public" val Public = "public"
} }
implicit val encodeKeyStorage: Encoder[KeyStore] = (ks: KeyStore) // Codec for a tuple of already serialized public and secret keys to json
Json.obj( private val pubSecJsonCodec: PureCodec[(String, String), Json] =
( CirceCodecs.circeJsonCodec(
Field.Keystore, {
Json.obj( case (pub, sec)
(Field.Secret, Json.fromString(ks.keyPair.secretKey.value.toBase64(alphabet))), Json.obj(
(Field.Public, Json.fromString(ks.keyPair.publicKey.value.toBase64(alphabet))) (
) Field.Keystore,
) Json.obj(
) (Field.Secret, Json.fromString(sec)),
(Field.Public, Json.fromString(pub))
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.")
) )
},
(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 { ) andThen (vecToStr split vecToStr) andThen pubSecJsonCodec
case Right(Some(ks)) EitherT.pure[F, IllegalArgumentException](ks)
case Right(None) implicit val keyPairJsonStringCodec: PureCodec[KeyPair, String] =
EitherT.leftT[F, KeyStore](new IllegalArgumentException("'" + base64 + "' is not a valid key store.")) keyPairJsonCodec andThen CirceCodecs.circeJsonParseCodec
case Left(err)
EitherT.leftT[F, KeyStore](new IllegalArgumentException("'" + base64 + "' is not a valid key store.", err))
}
} yield keyStore
} }

View File

@ -19,57 +19,30 @@ package fluence.crypto.keystore
import fluence.crypto.KeyPair import fluence.crypto.KeyPair
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
import scodec.bits.{Bases, ByteVector}
class KeyStoreSpec extends WordSpec with Matchers { class KeyStoreSpec extends WordSpec with Matchers {
private val alphabet = Bases.Alphabets.Base64Url private val keyPair = KeyPair.fromBytes("pubKey".getBytes, "secKey".getBytes)
private val keyStore = KeyStore(KeyPair.fromBytes("pubKey".getBytes, "secKey".getBytes))
private val jsonString = """{"keystore":{"secret":"c2VjS2V5","public":"cHViS2V5"}}""" private val jsonString = """{"keystore":{"secret":"c2VjS2V5","public":"cHViS2V5"}}"""
"KeyStore.encodeKeyStorage" should { "KeyStore.encodeKeyStorage" should {
"transform KeyStore to json" in { "transform KeyStore to json" in {
val result = KeyStore.encodeKeyStorage(keyStore) val result = KeyStore.keyPairJsonStringCodec.direct.unsafe(keyPair)
result.noSpaces shouldBe jsonString result shouldBe jsonString
} }
} }
"KeyStore.decodeKeyStorage" should { "KeyStore.decodeKeyStorage" should {
"transform KeyStore to json" in { "transform KeyStore to json" in {
import io.circe.parser._ val result = KeyStore.keyPairJsonStringCodec.inverse.unsafe(jsonString)
val result = KeyStore.decodeKeyStorage.decodeJson(parse(jsonString).right.get) result shouldBe keyPair
result.right.get.get shouldBe keyStore
} }
} }
"KeyStore" should { "KeyStore" should {
"transform KeyStore to json and back" in { "transform KeyStore to json and back" in {
val result = KeyStore.decodeKeyStorage.decodeJson(KeyStore.encodeKeyStorage(keyStore)) val result = KeyStore.keyPairJsonCodec.inverse.unsafe(KeyStore.keyPairJsonCodec.direct.unsafe(keyPair))
result.right.get.get shouldBe keyStore result shouldBe keyPair
}
}
"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
} }
} }