diff --git a/build.sbt b/build.sbt index 38ee008..fd1fa03 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -val scala3: String = "3.8.1" +val scala3: String = "3.8.2" ThisBuild / scalaVersion := scala3 ThisBuild / versionScheme := Some("semver-spec") @@ -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" @@ -52,7 +53,7 @@ lazy val testSettings = Seq( lazy val `gs-crypto` = project .in(file(".")) - .aggregate(core, argon2, rsa) + .aggregate(core, argon2, rsa, eddsa) .settings(noPublishSettings) .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") @@ -65,7 +66,8 @@ lazy val core = project ) .settings( libraryDependencies ++= Seq( - Deps.Cats.Effect + Deps.Cats.Effect, + Deps.BouncyCastle.PKIX ) ) @@ -97,3 +99,18 @@ lazy val rsa = project Deps.Cats.Effect ) ) + +lazy val eddsa = project + .in(file("modules/eddsa")) + .dependsOn(core) + .settings(sharedSettings) + .settings(testSettings) + .settings( + name := s"${gsProjectName.value}-eddsa-v${semVerMajor.value}" + ) + .settings( + libraryDependencies ++= Seq( + Deps.Cats.Effect, + Deps.BouncyCastle.Provider + ) + ) diff --git a/modules/core/src/main/scala/gs/crypto/v0/Base64Decoder.scala b/modules/core/src/main/scala/gs/crypto/v0/Base64Decoder.scala index 97680c6..271c6ff 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/Base64Decoder.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/Base64Decoder.scala @@ -3,9 +3,12 @@ package gs.crypto.v0 import java.{util => ju} /** Implementation of [[Decoder]] for Base64 strings. + * + * Supports base64-url decoding as well. */ object Base64Decoder extends Decoder[B64]: - private lazy val d: ju.Base64.Decoder = ju.Base64.getDecoder() + private lazy val d: ju.Base64.Decoder = ju.Base64.getDecoder() + private lazy val du: ju.Base64.Decoder = ju.Base64.getUrlDecoder() /** @inheritDocs */ @@ -21,3 +24,13 @@ object Base64Decoder extends Decoder[B64]: */ def decodeUnsafe(input: String): Array[Byte] = d.decode(input) + + /** Decode the base64-url encoded input. + * + * @param input + * The base64-url encoded data. + * @return + * The decoded bytes. + */ + def decodeUrl(input: B64Url): Array[Byte] = + du.decode(input.data) diff --git a/modules/core/src/main/scala/gs/crypto/v0/Base64Encoder.scala b/modules/core/src/main/scala/gs/crypto/v0/Base64Encoder.scala index 525fc72..24bcaf3 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/Base64Encoder.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/Base64Encoder.scala @@ -5,9 +5,12 @@ import java.nio.charset.StandardCharsets import java.util.Base64 /** Implementation of [[Encoder]] for Base64. + * + * Supports base64-url encoding as well. */ object Base64Encoder extends Encoder[B64]: - private lazy val e: Base64.Encoder = Base64.getEncoder() + private lazy val e: Base64.Encoder = Base64.getEncoder() + private lazy val eu: Base64.Encoder = Base64.getUrlEncoder() /** @inheritDocs */ @@ -21,3 +24,28 @@ object Base64Encoder extends Encoder[B64]: charset: Charset = StandardCharsets.UTF_8 ): B64 = encode(input.getBytes(charset)) + + /** Encode the given bytes using base64-url. + * + * @param input + * The input data. + * @return + * The base64-url-encoded string. + */ + def encodeUrl(input: Array[Byte]): B64Url = + B64Url(eu.encodeToString(input)) + + /** Encode the given bytes using base64-url. + * + * @param input + * The input data. + * @param charset + * The character set of the input data. + * @return + * The base64-url-encoded string. + */ + def encodeUrl( + input: String, + charset: Charset = StandardCharsets.UTF_8 + ): B64Url = + encodeUrl(input.getBytes(charset)) diff --git a/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala new file mode 100644 index 0000000..f8a8afa --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala @@ -0,0 +1,122 @@ +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[_]: 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]] = + scala.util + .Try { + config.decoder.decode(trim(collapse(unwrap(base)))) + } + .toEither + .left + .map(_ => KeyLoadError.DecodingFailure(config.encodingName)) + + private def unwrap(base: String): String = + config.wrappers.foldLeft(base) { + ( + acc, + w + ) => acc.replace(w, "") + } + + private def collapse(base: String): String = + if config.shouldCollapse then base.replace("\n", "") else base + + private def trim(base: String): String = + if config.shouldTrim then base.trim() else base + +object BaseKeyLoader: + + /** Configuration for loading keys. + * + * @param wrappers + * Values that wrap keys and must be removed. + * @param shouldCollapse + * Whether newlines should be collapsed. + * @param shouldTrim + * Whether the string data should be trimmed. + * @param encodingName + * The name of the encoding. + * @param decoder + * The decoder to be used on the pre-processed input. + */ + case class Config( + wrappers: List[String], + shouldCollapse: Boolean, + shouldTrim: Boolean, + encodingName: String, + decoder: Decoder[String] + ) + + /** Default configuration values that work in most cases. + */ + object Defaults: + + /** By default, generic begin/end public/private key wrappers are included. + */ + val Wrappers: List[String] = List( + "-----BEGIN PUBLIC KEY-----", + "-----END PUBLIC KEY-----", + "-----BEGIN PRIVATE KEY-----", + "-----END PRIVATE KEY-----" + ) + + /** By default, stringified key data has newlines removed. + */ + val ShouldCollapse: Boolean = true + + /** By default, stringified key data is trimmed. + */ + val ShouldTrim: Boolean = true + + /** The default encoding name is 'base64'. + */ + val EncodingName: String = "base64" + + /** The default configuration handles standard wrapped, base64-encoded keys. + */ + val Config: BaseKeyLoader.Config = BaseKeyLoader.Config( + wrappers = Wrappers, + shouldCollapse = ShouldCollapse, + shouldTrim = ShouldTrim, + encodingName = EncodingName, + decoder = s => Base64Decoder.decodeUnsafe(s) + ) + + end Defaults + +end BaseKeyLoader diff --git a/modules/core/src/main/scala/gs/crypto/v0/Encoded.scala b/modules/core/src/main/scala/gs/crypto/v0/Encoded.scala index a80629e..98c615d 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/Encoded.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/Encoded.scala @@ -23,6 +23,11 @@ trait Encoded: * The encoded data. */ +/** Represents Base64-encoded data. + * + * @param data + * The encoded data. + */ final class B64( val data: String ) extends Encoded: @@ -63,6 +68,51 @@ object B64: end B64 +/** Represents Base64-url-encoded data. + * + * @param data + * The encoded data. + */ +final class B64Url( + val data: String +) extends Encoded: + /** @inheritDocs + */ + def decode(): Array[Byte] = Base64Decoder.decodeUrl(this) + + /** @inheritDocs + */ + override def equals(obj: Any): Boolean = + obj match + case other: B64Url => data == other.data + + /** @inheritDocs + */ + override def hashCode(): Int = data.hashCode() + + /** @inheritDocs + */ + override def toString(): String = data + +object B64Url: + + /** Instantiate [[B64Url]] from the given string. + * + * This function does NOT validate the input. + * + * @param data + * The encoded data. + * @return + * The new [[B64Url]] instance. + */ + def apply( + data: String + ): B64Url = new B64Url(data) + + given CanEqual[B64Url, B64Url] = CanEqual.derived + +end B64Url + /** Represents Hex-encoded data. * * @param data diff --git a/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala new file mode 100644 index 0000000..22d27fa --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala @@ -0,0 +1,30 @@ +package gs.crypto.v0 + +import cats.effect.Sync +import cats.syntax.all.* +import java.nio.file.Files +import java.nio.file.Path + +/** Implementation of [[KeyLoader]] that loads keys from encoded string files. + * + * @param config + * The key loader configuration. + * @param charset + * The expected character set for the loaded file. + */ +class FileKeyLoader[F[_]: Sync]( + val config: BaseKeyLoader.Config +) extends BaseKeyLoader[F]: + + /** @inheritDocs + */ + override def loadKey( + location: String + ): F[Either[KeyLoadError, Array[Byte]]] = + val p = Path.of(location) + Sync[F].delay(Files.isRegularFile(p)).flatMap { + case false => + Sync[F].pure(Left(KeyLoadError.PathNotRegularFile(p, location))) + case true => + Sync[F].delay(Files.readString(p)).map(prepareKey) + } diff --git a/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala b/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala new file mode 100644 index 0000000..8e8e1e9 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala @@ -0,0 +1,16 @@ +package gs.crypto.v0 + +import java.nio.file.Path + +sealed trait KeyLoadError + +object KeyLoadError: + + case class PathNotRegularFile( + path: Path, + location: String + ) extends KeyLoadError + + case class DecodingFailure(encodedFormat: String) extends KeyLoadError + +end KeyLoadError diff --git a/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala new file mode 100644 index 0000000..c0b8aba --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala @@ -0,0 +1,61 @@ +package gs.crypto.v0 + +import cats.effect.Sync +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. 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 key's raw bytes - ready for further decoding. + */ + def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]] + +object KeyLoader: + + def stringLoader[F[_]: Sync]( + config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config + ): StringKeyLoader[F] = + new StringKeyLoader(config) + + def fileLoader[F[_]: Sync]( + config: BaseKeyLoader.Config = BaseKeyLoader.Defaults.Config + ): FileKeyLoader[F] = + new FileKeyLoader(config) + + def resourceLoader[F[_]: Sync]( + 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/ResourceKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/ResourceKeyLoader.scala new file mode 100644 index 0000000..afefa4d --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/ResourceKeyLoader.scala @@ -0,0 +1,26 @@ +package gs.crypto.v0 + +import cats.effect.Resource +import cats.effect.Sync +import scala.io.Source + +/** Implementation of [[KeyLoader]] that loads keys from encoded string files + * that exist as project resources. + * + * @param config + * The key loader configuration. + */ +class ResourceKeyLoader[F[_]: Sync]( + val config: BaseKeyLoader.Config +) extends BaseKeyLoader[F]: + + /** @inheritDocs + */ + override def loadKey( + location: String + ): F[Either[KeyLoadError, Array[Byte]]] = + Resource + .make(Sync[F].delay(Source.fromResource(location)))(source => + Sync[F].delay(source.close()) + ) + .use(source => Sync[F].delay(prepareKey(source.getLines().mkString))) diff --git a/modules/core/src/main/scala/gs/crypto/v0/Signature.scala b/modules/core/src/main/scala/gs/crypto/v0/Signature.scala new file mode 100644 index 0000000..dfb4b58 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/Signature.scala @@ -0,0 +1,51 @@ +package gs.crypto.v0 + +/** Represents a cryptographic signature. Opaque type for an array of bytes. + */ +opaque type Signature = Array[Byte] + +object Signature: + + /** Instantiate a new signature from the given byte array. + * + * @param value + * The signature value. + * @return + * The signature bytes. + */ + def apply(value: Array[Byte]): Signature = value + + /** Instantiate a new signature from the given encoded string. + * + * @param value + * The input data. + * @return + * The new signature. + */ + def fromEncoded(value: Encoded): Signature = + value.decode() + + given CanEqual[Signature, Signature] = CanEqual.derived + + extension (sig: Signature) + /** @return + * The underlying byte array. + */ + def unwrap(): Array[Byte] = sig + + /** @return + * This signature, encoded using base64. + */ + def toBase64(): B64 = Base64Encoder.encode(sig) + + /** @return + * This signature, encoded using base64-url. + */ + def toBase64Url(): B64Url = Base64Encoder.encodeUrl(sig) + + /** @return + * This signature, encoded using hex. + */ + def toHex(): Hex = HexEncoder.encode(sig) + +end Signature diff --git a/modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala b/modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala new file mode 100644 index 0000000..f836aaf --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala @@ -0,0 +1,50 @@ +package gs.crypto.v0 + +/** Used to communicate the calculated validity of some [[Signature]]. + * + * @param name + * The enumeration name. + */ +sealed abstract class SignatureValidity(val name: String): + + /** @inheritDocs + */ + override def equals(obj: Any): Boolean = + obj match + case other: SignatureValidity => name == other.name + case _ => false + + /** @inheritDocs + */ + override def hashCode(): Int = name.hashCode() + + /** @inheritDocs + */ + override def toString(): String = name + +object SignatureValidity: + + /** Map a Boolean value to validity. + * + * - True: Valid + * - False: Invalid + * + * @param isValid + * Whether the signature is valid or not. + * @return + * The [[SignatureValidity]] selected for the given validity. + */ + def apply(isValid: Boolean): SignatureValidity = + if isValid then Valid else Invalid + + given CanEqual[SignatureValidity, SignatureValidity] = CanEqual.derived + + /** The [[Signature]] is valid. + */ + case object Valid extends SignatureValidity("valid") + + /** The [[Signature]] is invalid. + */ + case object Invalid extends SignatureValidity("invalid") + +end SignatureValidity diff --git a/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala b/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala new file mode 100644 index 0000000..5a497ee --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala @@ -0,0 +1,10 @@ +package gs.crypto.v0 + +/** Used to verify cryptographic signatures. + */ +trait SignatureVerifier[F[_]]: + + def verify( + signature: Signature, + data: Array[Byte] + ): F[SignatureValidity] diff --git a/modules/core/src/main/scala/gs/crypto/v0/Signer.scala b/modules/core/src/main/scala/gs/crypto/v0/Signer.scala new file mode 100644 index 0000000..3b387f3 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/Signer.scala @@ -0,0 +1,30 @@ +package gs.crypto.v0 + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +/** Used to calculate cryptographic signatures. + */ +trait Signer[F[_]]: + /** Calculate a signature for the given data. + * + * @param data + * The input data. + * @return + * The calculated signature. + */ + def sign(data: Array[Byte]): F[Signature] + + /** Calculate a signature for the given data. + * + * @param data + * The input data. + * @param charset + * The character set of the input data. + * @return + * The calculated signature. + */ + def sign( + data: String, + charset: Charset = StandardCharsets.UTF_8 + ): F[Signature] diff --git a/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala b/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala new file mode 100644 index 0000000..46cbc2a --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala @@ -0,0 +1,15 @@ +package gs.crypto.v0 + +import cats.effect.Sync + +/** Implementation of [[KeyLoader]] that loads keys directly from input strings. + * + * @param config + * The key loader configuration. + */ +class StringKeyLoader[F[_]: Sync]( + val config: BaseKeyLoader.Config +) extends BaseKeyLoader[F]: + + 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 new file mode 100644 index 0000000..7ae148b --- /dev/null +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala @@ -0,0 +1,38 @@ +package gs.crypto.v0.eddsa + +import cats.effect.Sync +import gs.crypto.v0.Signature +import gs.crypto.v0.Signer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.security.PrivateKey + +/** Implementation of [[Signer]] that uses the Ed25519 system to calculate + * signatures. + * + * See: [[Ed25519Verifier]] + * + * @param privateKey + * The private key. + */ +final class Ed25519Signer[F[_]: Sync]( + privateKey: PrivateKey +) extends Signer[F]: + + /** @inheritDocs + */ + override def sign(data: Array[Byte]): F[Signature] = + Sync[F].delay { + val s = java.security.Signature.getInstance(Ed25519.Algorithm) + val _ = s.initSign(privateKey) + val _ = s.update(data) + Signature(s.sign()) + } + + /** @inheritDocs + */ + override def sign( + data: String, + charset: Charset = StandardCharsets.UTF_8 + ): F[Signature] = + sign(data.getBytes(charset)) 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 new file mode 100644 index 0000000..2107e0f --- /dev/null +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala @@ -0,0 +1,32 @@ +package gs.crypto.v0.eddsa + +import cats.effect.Sync +import gs.crypto.v0.Signature +import gs.crypto.v0.SignatureValidity +import gs.crypto.v0.SignatureVerifier +import java.security.PublicKey + +/** Implementation of [[SignatureVerifier]] that uses the Ed25519 system to + * verify signatures. + * + * See: [[Ed25519Signer]] + * + * @param publicKey + * The public key. + */ +final class Ed25519Verifier[F[_]: Sync]( + publicKey: PublicKey +) extends SignatureVerifier[F]: + + /** @inheritDocs + */ + override def verify( + signature: Signature, + data: Array[Byte] + ): F[SignatureValidity] = + Sync[F].delay { + 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/resources/private.pem b/modules/eddsa/src/test/resources/private.pem new file mode 100644 index 0000000..6afc843 --- /dev/null +++ b/modules/eddsa/src/test/resources/private.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIPkucDMoZPkyltc200nNyyfJKWP7cu3Tr9gdsFB6PeqN +-----END PRIVATE KEY----- diff --git a/modules/eddsa/src/test/resources/public.pem b/modules/eddsa/src/test/resources/public.pem new file mode 100644 index 0000000..d457336 --- /dev/null +++ b/modules/eddsa/src/test/resources/public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAYNI09OpzbZ9GiKhZzNEbwWYISkR2ihfvWEsZx62b+bY= +-----END PUBLIC KEY----- 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 diff --git a/project/build.properties b/project/build.properties index 4d6c567..c9aaa51 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.12.2 +sbt.version=1.12.4