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()
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
}
}
}

View File

@ -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)))
}

View File

@ -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
}

View File

@ -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
}
}