diff --git a/build.sbt b/build.sbt index 4a54b68..77b9139 100644 --- a/build.sbt +++ b/build.sbt @@ -33,11 +33,12 @@ val Deps = new { } val Gs = new { - val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.1.1" + val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1" } val BouncyCastle = new { val Provider: ModuleID = "org.bouncycastle" % "bcprov-jdk18on" % "1.83" + val PKIX: ModuleID = "org.bouncycastle" % "bcpkix-jdk18on" % "1.83" } val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.2" @@ -65,7 +66,8 @@ lazy val core = project ) .settings( libraryDependencies ++= Seq( - Deps.Cats.Effect + Deps.Cats.Effect, + Deps.BouncyCastle.PKIX ) ) diff --git a/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala index 52e1873..f8a8afa 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala @@ -1,12 +1,39 @@ package gs.crypto.v0 +import cats.effect.Sync +import cats.syntax.all.* +import java.security.PrivateKey +import java.security.PublicKey + /** Base implementation for [[KeyLoader]] that provides standard parsing * functionality. */ -abstract class BaseKeyLoader[F[_]] extends KeyLoader[F]: - /** @return - * The key loader configuration. - */ +abstract class BaseKeyLoader[F[_]: Sync] extends KeyLoader[F]: + + def loadPublicKey( + location: String, + algorithm: String + ): F[Either[KeyLoadError, PublicKey]] = + loadKey(location).flatMap { + case Left(err) => Sync[F].pure(Left(err)) + case Right(key) => + KeyLoader.loadPublicKey[F](key, algorithm).map(Right(_)) + } + + def loadPrivateKey( + location: String, + algorithm: String + ): F[Either[KeyLoadError, PrivateKey]] = + loadKey(location).flatMap { + case Left(err) => Sync[F].pure(Left(err)) + case Right(key) => + KeyLoader.loadPrivateKey[F](key, algorithm).map(Right(_)) + } + + /** @return + * The key loader configuration. + */ + def config: BaseKeyLoader.Config protected def prepareKey(base: String): Either[KeyLoadError, Array[Byte]] = diff --git a/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala index 54b0874..22d27fa 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala @@ -2,7 +2,6 @@ package gs.crypto.v0 import cats.effect.Sync import cats.syntax.all.* -import java.nio.charset.Charset import java.nio.file.Files import java.nio.file.Path @@ -14,8 +13,7 @@ import java.nio.file.Path * The expected character set for the loaded file. */ class FileKeyLoader[F[_]: Sync]( - val config: BaseKeyLoader.Config, - val charset: Charset + val config: BaseKeyLoader.Config ) extends BaseKeyLoader[F]: /** @inheritDocs @@ -28,5 +26,5 @@ class FileKeyLoader[F[_]: Sync]( case false => Sync[F].pure(Left(KeyLoadError.PathNotRegularFile(p, location))) case true => - Sync[F].delay(Files.readString(p, charset)).map(prepareKey) + Sync[F].delay(Files.readString(p)).map(prepareKey) } diff --git a/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala index 65f5027..c0b8aba 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala @@ -1,38 +1,61 @@ package gs.crypto.v0 -import cats.Applicative import cats.effect.Sync -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.PublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec /** Interface for loading keys (e.g. public/private keys) into memory. */ trait KeyLoader[F[_]]: - /** Load the key from the given location. + /** Load the key from the given location. This gets the raw bytes, but does + * not handle _any_ standard encodings such as PKCS8 or X509. These are + * handled by helper functions (static) or [[BaseKeyLoader]]. * * @param location * The location of the key. * @return - * The loaded key. + * The key's raw bytes - ready for further decoding. */ def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]] object KeyLoader: - def stringLoader[F[_]: Applicative]( - config: BaseKeyLoader.Config + def stringLoader[F[_]: Sync]( + config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config ): StringKeyLoader[F] = - new StringKeyLoader[F](config) + new StringKeyLoader(config) def fileLoader[F[_]: Sync]( - config: BaseKeyLoader.Config, - charset: Charset = StandardCharsets.UTF_8 + config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config ): FileKeyLoader[F] = - new FileKeyLoader(config, charset) + new FileKeyLoader(config) def resourceLoader[F[_]: Sync]( - config: BaseKeyLoader.Config + config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config ): ResourceKeyLoader[F] = new ResourceKeyLoader(config) + def loadPublicKey[F[_]: Sync]( + publicKeyRawBytes: Array[Byte], + algorithm: String + ): F[PublicKey] = + Sync[F].delay { + val spec = new X509EncodedKeySpec(publicKeyRawBytes) + val keyFactory = KeyFactory.getInstance(algorithm) + keyFactory.generatePublic(spec) + } + + def loadPrivateKey[F[_]: Sync]( + privateKeyRawBytes: Array[Byte], + algorithm: String + ): F[PrivateKey] = + Sync[F].delay { + val spec = new PKCS8EncodedKeySpec(privateKeyRawBytes) + val keyFactory = KeyFactory.getInstance(algorithm) + keyFactory.generatePrivate(spec) + } + end KeyLoader diff --git a/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala b/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala index 713ee32..5a497ee 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala @@ -3,4 +3,8 @@ package gs.crypto.v0 /** Used to verify cryptographic signatures. */ trait SignatureVerifier[F[_]]: - def verify(signature: Signature): F[SignatureValidity] + + def verify( + signature: Signature, + data: Array[Byte] + ): F[SignatureValidity] diff --git a/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala index eb458c7..46cbc2a 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala @@ -1,19 +1,15 @@ package gs.crypto.v0 -import cats.Applicative +import cats.effect.Sync /** Implementation of [[KeyLoader]] that loads keys directly from input strings. * * @param config * The key loader configuration. */ -class StringKeyLoader[F[_]: Applicative]( +class StringKeyLoader[F[_]: Sync]( val config: BaseKeyLoader.Config ) extends BaseKeyLoader[F]: - /** @inheritDocs - */ - override def loadKey( - location: String - ): F[Either[KeyLoadError, Array[Byte]]] = - Applicative[F].pure(prepareKey(location)) + override def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]] = + Sync[F].delay(prepareKey(location)) diff --git a/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519.scala b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519.scala new file mode 100644 index 0000000..6ea343c --- /dev/null +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519.scala @@ -0,0 +1,7 @@ +package gs.crypto.v0.eddsa + +object Ed25519: + + val Algorithm: String = "Ed25519" + +end Ed25519 diff --git a/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala index b10cfbe..7ae148b 100644 --- a/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala @@ -5,7 +5,7 @@ import gs.crypto.v0.Signature import gs.crypto.v0.Signer import java.nio.charset.Charset import java.nio.charset.StandardCharsets -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import java.security.PrivateKey /** Implementation of [[Signer]] that uses the Ed25519 system to calculate * signatures. @@ -16,18 +16,17 @@ import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters * The private key. */ final class Ed25519Signer[F[_]: Sync]( - privateKey: Array[Byte] + privateKey: PrivateKey ) extends Signer[F]: - private lazy val params = new Ed25519PrivateKeyParameters(privateKey) /** @inheritDocs */ override def sign(data: Array[Byte]): F[Signature] = Sync[F].delay { - val s = new org.bouncycastle.crypto.signers.Ed25519Signer() - val _ = s.init(true, params) - val _ = s.update(data, 0, data.length) - Signature(s.generateSignature()) + val s = java.security.Signature.getInstance(Ed25519.Algorithm) + val _ = s.initSign(privateKey) + val _ = s.update(data) + Signature(s.sign()) } /** @inheritDocs diff --git a/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala index 5a00dd4..c054aea 100644 --- a/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala @@ -4,7 +4,7 @@ import cats.effect.Sync import gs.crypto.v0.Signature import gs.crypto.v0.SignatureValidity import gs.crypto.v0.SignatureVerifier -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import java.security.PublicKey /** Implementation of [[SignatureVerifier]] that uses the Ed25519 system to * verify signatures. @@ -15,15 +15,18 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters * The public key. */ final class Ed25519Verifier[F[_]: Sync]( - publicKey: Array[Byte] + publicKey: PublicKey ) extends SignatureVerifier[F]: - private lazy val params = new Ed25519PublicKeyParameters(publicKey) /** @inheritDocs */ - override def verify(signature: Signature): F[SignatureValidity] = + override def verify( + signature: Signature, + data: Array[Byte] // TODO: CLEAN UP + ): F[SignatureValidity] = Sync[F].delay { - val s = new org.bouncycastle.crypto.signers.Ed25519Signer() - val _ = s.init(false, params) - SignatureValidity(s.verifySignature(signature.unwrap())) + val s = java.security.Signature.getInstance(Ed25519.Algorithm) + val _ = s.initVerify(publicKey) + val _ = s.update(data) + SignatureValidity(s.verify(signature.unwrap())) } diff --git a/modules/eddsa/src/test/scala/gs/crypto/v0/eddsa/Ed25519Tests.scala b/modules/eddsa/src/test/scala/gs/crypto/v0/eddsa/Ed25519Tests.scala new file mode 100644 index 0000000..1d977bf --- /dev/null +++ b/modules/eddsa/src/test/scala/gs/crypto/v0/eddsa/Ed25519Tests.scala @@ -0,0 +1,61 @@ +package gs.crypto.v0.eddsa + +import cats.effect.IO +import cats.effect.unsafe.IORuntime +import gs.crypto.v0.KeyLoader +import gs.crypto.v0.SignatureValidity +import java.security.PrivateKey +import java.security.PublicKey +import java.util.UUID +import munit.Location + +class Ed25519Tests extends munit.FunSuite: + import Ed25519Tests.Resources + + given IORuntime = IORuntime.global + + def iotest( + name: String + )( + f: => IO[Unit] + )( + using + Location + ): Unit = + test(name)(f.unsafeRunSync()) + + val keyLoader = KeyLoader.resourceLoader[IO]() + + iotest( + "should sign some data and verify that signature" + ) { + val data = UUID.randomUUID().toString() + loadKeysOrFail().flatMap { case (publicKey, privateKey) => + val signer = new Ed25519Signer[IO](privateKey) + val verifier = new Ed25519Verifier[IO](publicKey) + + for + signature <- signer.sign(data) + validity <- verifier.verify(signature, data.getBytes()) + yield assertEquals(validity, SignatureValidity.Valid) + } + } + + private def loadKeysOrFail(): IO[(PublicKey, PrivateKey)] = + keyLoader.loadPublicKey(Resources.PublicKey, Ed25519.Algorithm).flatMap { + case Left(_) => fail("Failed to load public key.") + case Right(pub) => + keyLoader.loadPrivateKey(Resources.PrivateKey, Ed25519.Algorithm).map { + case Left(_) => fail("Failed to load private key.") + case Right(priv) => (pub, priv) + } + } + +object Ed25519Tests: + + object Resources: + val PublicKey: String = "public.pem" + val PrivateKey: String = "private.pem" + end Resources + +end Ed25519Tests