mirror of
https://github.com/fluencelabs/crypto
synced 2025-04-24 14:22:18 +00:00
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:
parent
88034b371f
commit
c27a1865c5
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user