From 503569c475d708d16d0c84f96064c0c6841bff25 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 15 Feb 2026 19:53:51 -0600 Subject: [PATCH 1/4] (minor) adding eddsa, more core support --- build.sbt | 17 +++- .../scala/gs/crypto/v0/Base64Decoder.scala | 12 +++ .../scala/gs/crypto/v0/Base64Encoder.scala | 25 ++++++ .../scala/gs/crypto/v0/BaseKeyLoader.scala | 89 +++++++++++++++++++ .../src/main/scala/gs/crypto/v0/Encoded.scala | 50 +++++++++++ .../scala/gs/crypto/v0/FileKeyLoader.scala | 30 +++++++ .../scala/gs/crypto/v0/KeyLoadError.scala | 12 +++ .../main/scala/gs/crypto/v0/KeyLoader.scala | 38 ++++++++ .../gs/crypto/v0/ResourceKeyLoader.scala | 27 ++++++ .../main/scala/gs/crypto/v0/Signature.scala | 50 +++++++++++ .../gs/crypto/v0/SignatureValidity.scala | 47 ++++++++++ .../gs/crypto/v0/SignatureVerifier.scala | 7 ++ .../src/main/scala/gs/crypto/v0/Signer.scala | 25 ++++++ .../scala/gs/crypto/v0/StringKeyLoader.scala | 18 ++++ .../gs/crypto/v0/eddsa/Ed25519Signer.scala | 37 ++++++++ .../gs/crypto/v0/eddsa/Ed25519Verifier.scala | 28 ++++++ modules/eddsa/src/test/resources/private.pem | 3 + modules/eddsa/src/test/resources/public.pem | 3 + 18 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/ResourceKeyLoader.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/Signature.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/Signer.scala create mode 100644 modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala create mode 100644 modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala create mode 100644 modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala create mode 100644 modules/eddsa/src/test/resources/private.pem create mode 100644 modules/eddsa/src/test/resources/public.pem diff --git a/build.sbt b/build.sbt index 38ee008..4a54b68 100644 --- a/build.sbt +++ b/build.sbt @@ -52,7 +52,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}") @@ -97,3 +97,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..a05f95e 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 du: ju.Base64.Decoder = ju.Base64.getUrlDecoder() /** @inheritDocs */ @@ -21,3 +24,12 @@ 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..5102187 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 eu: Base64.Encoder = Base64.getUrlEncoder() /** @inheritDocs */ @@ -21,3 +24,25 @@ 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..1bc42e8 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala @@ -0,0 +1,89 @@ +package gs.crypto.v0 + +/** + * Base implementation for [[KeyLoader]] that provides standard parsing + * functionality. + */ +abstract class BaseKeyLoader[F[_]] extends KeyLoader[F]: + /** + * @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..147dacd --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala @@ -0,0 +1,30 @@ +package gs.crypto.v0 + +import cats.syntax.all.* +import cats.effect.Sync +import java.nio.file.Files +import java.nio.file.Path +import java.nio.charset.Charset + +/** + * 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, + val charset: Charset +) 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, charset)).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..c3c23b6 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala @@ -0,0 +1,12 @@ +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..b7297e2 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala @@ -0,0 +1,38 @@ +package gs.crypto.v0 + +import cats.Applicative +import cats.effect.Sync +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +/** + * Interface for loading keys (e.g. public/private keys) into memory. + */ +trait KeyLoader[F[_]]: + /** + * Load the key from the given location. + * + * @param location The location of the key. + * @return The loaded key. + */ + def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]] + +object KeyLoader: + + def stringLoader[F[_]: Applicative]( + config: BaseKeyLoader.Config + ): StringKeyLoader[F] = + new StringKeyLoader[F](config) + + def fileLoader[F[_]: Sync]( + config: BaseKeyLoader.Config, + charset: Charset = StandardCharsets.UTF_8 + ): FileKeyLoader[F] = + new FileKeyLoader(config, charset) + + def resourceLoader[F[_]: Sync]( + config: BaseKeyLoader.Config + ): ResourceKeyLoader[F] = + new ResourceKeyLoader(config) + +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..029183b --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/ResourceKeyLoader.scala @@ -0,0 +1,27 @@ +package gs.crypto.v0 + +import cats.effect.Sync +import cats.effect.Resource +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..841ab5d --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/Signature.scala @@ -0,0 +1,50 @@ +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..fec6c38 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala @@ -0,0 +1,47 @@ +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..4975236 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala @@ -0,0 +1,7 @@ +package gs.crypto.v0 + +/** + * Used to verify cryptographic signatures. + */ +trait SignatureVerifier[F[_]]: + def verify(signature: Signature): 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..873d223 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/Signer.scala @@ -0,0 +1,25 @@ +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..e9251c7 --- /dev/null +++ b/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala @@ -0,0 +1,18 @@ +package gs.crypto.v0 + +import cats.Applicative + +/** + * Implementation of [[KeyLoader]] that loads keys directly from input strings. + * + * @param config The key loader configuration. + */ +class StringKeyLoader[F[_]: Applicative]( + val config: BaseKeyLoader.Config +) extends BaseKeyLoader[F]: + + /** @inheritDocs */ + override def loadKey( + location: String + ): F[Either[KeyLoadError, Array[Byte]]] = + Applicative[F].pure(prepareKey(location)) 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..268c117 --- /dev/null +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Signer.scala @@ -0,0 +1,37 @@ +package gs.crypto.v0.eddsa + +import cats.effect.Sync +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import gs.crypto.v0.Signature +import gs.crypto.v0.Signer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets + +/** + * Implementation of [[Signer]] that uses the Ed25519 system to calculate + * signatures. + * + * See: [[Ed25519Verifier]] + * + * @param privateKey The private key. + */ +final class Ed25519Signer[F[_]: Sync]( + privateKey: Array[Byte] +) 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()) + } + + /** @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..3057e85 --- /dev/null +++ b/modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala @@ -0,0 +1,28 @@ +package gs.crypto.v0.eddsa + +import cats.effect.Sync +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters +import gs.crypto.v0.Signature +import gs.crypto.v0.SignatureVerifier +import gs.crypto.v0.SignatureValidity + +/** + * Implementation of [[SignatureVerifier]] that uses the Ed25519 system to + * verify signatures. + * + * See: [[Ed25519Signer]] + * + * @param publicKey The public key. + */ +final class Ed25519Verifier[F[_]: Sync]( + publicKey: Array[Byte] +) extends SignatureVerifier[F]: + private lazy val params = new Ed25519PublicKeyParameters(publicKey) + + /** @inheritDocs */ + override def verify(signature: Signature): F[SignatureValidity] = + Sync[F].delay { + val s = new org.bouncycastle.crypto.signers.Ed25519Signer() + val _ = s.init(false, params) + SignatureValidity(s.verifySignature(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----- -- 2.43.0 From f4bbd53d44f1ed2bce46a1466004a0dca3683ddc Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 15 Feb 2026 19:54:22 -0600 Subject: [PATCH 2/4] (patch) scalafmt --- .../scala/gs/crypto/v0/Base64Decoder.scala | 19 ++--- .../scala/gs/crypto/v0/Base64Encoder.scala | 35 ++++---- .../scala/gs/crypto/v0/BaseKeyLoader.scala | 80 ++++++++++--------- .../scala/gs/crypto/v0/FileKeyLoader.scala | 18 +++-- .../scala/gs/crypto/v0/KeyLoadError.scala | 6 +- .../main/scala/gs/crypto/v0/KeyLoader.scala | 18 ++--- .../gs/crypto/v0/ResourceKeyLoader.scala | 17 ++-- .../main/scala/gs/crypto/v0/Signature.scala | 53 ++++++------ .../gs/crypto/v0/SignatureValidity.scala | 51 ++++++------ .../gs/crypto/v0/SignatureVerifier.scala | 3 +- .../src/main/scala/gs/crypto/v0/Signer.scala | 37 +++++---- .../scala/gs/crypto/v0/StringKeyLoader.scala | 11 +-- .../gs/crypto/v0/eddsa/Ed25519Signer.scala | 18 +++-- .../gs/crypto/v0/eddsa/Ed25519Verifier.scala | 15 ++-- 14 files changed, 204 insertions(+), 177 deletions(-) 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 a05f95e..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,11 +3,11 @@ package gs.crypto.v0 import java.{util => ju} /** Implementation of [[Decoder]] for Base64 strings. - * - * Supports base64-url decoding as well. + * + * 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 @@ -25,11 +25,12 @@ 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. - */ + /** 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 5102187..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,11 +5,11 @@ import java.nio.charset.StandardCharsets import java.util.Base64 /** Implementation of [[Encoder]] for Base64. - * - * Supports base64-url encoding as well. + * + * 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 @@ -25,22 +25,25 @@ object Base64Encoder extends Encoder[B64]: ): B64 = encode(input.getBytes(charset)) - /** - * Encode the given bytes using base64-url. - * - * @param input The input data. - * @return The base64-url-encoded string. - */ + /** 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. - */ + /** 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 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 1bc42e8..52e1873 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/BaseKeyLoader.scala @@ -1,22 +1,30 @@ package gs.crypto.v0 -/** - * Base implementation for [[KeyLoader]] that provides standard parsing +/** Base implementation for [[KeyLoader]] that provides standard parsing * functionality. */ abstract class BaseKeyLoader[F[_]] extends KeyLoader[F]: - /** - * @return The key loader configuration. - */ + /** @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)) + 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, "") } + config.wrappers.foldLeft(base) { + ( + acc, + w + ) => acc.replace(w, "") + } private def collapse(base: String): String = if config.shouldCollapse then base.replace("\n", "") else base @@ -26,15 +34,19 @@ abstract class BaseKeyLoader[F[_]] extends KeyLoader[F]: 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. - */ + /** 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, @@ -43,39 +55,33 @@ object BaseKeyLoader: decoder: Decoder[String] ) - /** - * Default configuration values that work in most cases. - */ + /** Default configuration values that work in most cases. + */ object Defaults: - /** - * By default, generic begin/end public/private key wrappers are included. - */ + /** 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-----", + "-----END PRIVATE KEY-----" ) - /** - * By default, stringified key data has newlines removed. - */ + /** By default, stringified key data has newlines removed. + */ val ShouldCollapse: Boolean = true - /** - * By default, stringified key data is trimmed. - */ + /** By default, stringified key data is trimmed. + */ val ShouldTrim: Boolean = true - /** - * The default encoding name is 'base64'. - */ + /** The default encoding name is 'base64'. + */ val EncodingName: String = "base64" - /** - * The default configuration handles standard wrapped, base64-encoded keys. - */ + /** The default configuration handles standard wrapped, base64-encoded keys. + */ val Config: BaseKeyLoader.Config = BaseKeyLoader.Config( wrappers = Wrappers, shouldCollapse = ShouldCollapse, 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 147dacd..54b0874 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/FileKeyLoader.scala @@ -1,26 +1,28 @@ package gs.crypto.v0 -import cats.syntax.all.* import cats.effect.Sync +import cats.syntax.all.* +import java.nio.charset.Charset import java.nio.file.Files import java.nio.file.Path -import java.nio.charset.Charset -/** - * Implementation of [[KeyLoader]] that loads keys from encoded string files. +/** 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. + * @param config + * The key loader configuration. + * @param charset + * The expected character set for the loaded file. */ class FileKeyLoader[F[_]: Sync]( val config: BaseKeyLoader.Config, val charset: Charset ) extends BaseKeyLoader[F]: - /** @inheritDocs */ + /** @inheritDocs + */ override def loadKey( location: String - ): F[Either[KeyLoadError, Array[Byte]]] = + ): F[Either[KeyLoadError, Array[Byte]]] = val p = Path.of(location) Sync[F].delay(Files.isRegularFile(p)).flatMap { case false => diff --git a/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala b/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala index c3c23b6..8e8e1e9 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoadError.scala @@ -6,7 +6,11 @@ sealed trait KeyLoadError object KeyLoadError: - case class PathNotRegularFile(path: Path, location: String) extends 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 index b7297e2..65f5027 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/KeyLoader.scala @@ -5,16 +5,16 @@ import cats.effect.Sync import java.nio.charset.Charset import java.nio.charset.StandardCharsets -/** - * Interface for loading keys (e.g. public/private keys) into memory. - */ +/** Interface for loading keys (e.g. public/private keys) into memory. + */ trait KeyLoader[F[_]]: - /** - * Load the key from the given location. - * - * @param location The location of the key. - * @return The loaded key. - */ + /** Load the key from the given location. + * + * @param location + * The location of the key. + * @return + * The loaded key. + */ def loadKey(location: String): F[Either[KeyLoadError, Array[Byte]]] object 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 index 029183b..afefa4d 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/ResourceKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/ResourceKeyLoader.scala @@ -1,27 +1,26 @@ package gs.crypto.v0 -import cats.effect.Sync import cats.effect.Resource +import cats.effect.Sync import scala.io.Source -/** - * Implementation of [[KeyLoader]] that loads keys from encoded string files +/** Implementation of [[KeyLoader]] that loads keys from encoded string files * that exist as project resources. * - * @param config The key loader configuration. + * @param config + * The key loader configuration. */ class ResourceKeyLoader[F[_]: Sync]( val config: BaseKeyLoader.Config ) extends BaseKeyLoader[F]: - /** @inheritDocs */ + /** @inheritDocs + */ override def loadKey( location: String - ): F[Either[KeyLoadError, Array[Byte]]] = + ): 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)) - } + .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 index 841ab5d..dfb4b58 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/Signature.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/Signature.scala @@ -1,50 +1,51 @@ package gs.crypto.v0 -/** - * Represents a cryptographic signature. Opaque type for an array of bytes. +/** 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. - */ + /** 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. - */ + /** 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. - */ + /** @return + * The underlying byte array. + */ def unwrap(): Array[Byte] = sig - /** - * @return This signature, encoded using base64. - */ + /** @return + * This signature, encoded using base64. + */ def toBase64(): B64 = Base64Encoder.encode(sig) - /** - * @return This signature, encoded using base64-url. - */ + /** @return + * This signature, encoded using base64-url. + */ def toBase64Url(): B64Url = Base64Encoder.encodeUrl(sig) - /** - * @return This signature, encoded using hex. - */ + /** @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 index fec6c38..f836aaf 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureValidity.scala @@ -1,47 +1,50 @@ package gs.crypto.v0 -/** - * Used to communicate the calculated validity of some [[Signature]]. +/** Used to communicate the calculated validity of some [[Signature]]. * - * @param name The enumeration name. + * @param name + * The enumeration name. */ sealed abstract class SignatureValidity(val name: String): - /** @inheritDocs */ - override def equals(obj: Any): Boolean = + + /** @inheritDocs + */ + override def equals(obj: Any): Boolean = obj match case other: SignatureValidity => name == other.name - case _ => false + case _ => false - /** @inheritDocs */ + /** @inheritDocs + */ override def hashCode(): Int = name.hashCode() - /** @inheritDocs */ + /** @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. - */ + /** 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. - */ + /** The [[Signature]] is valid. + */ case object Valid extends SignatureValidity("valid") - - /** - * The [[Signature]] is invalid. - */ + + /** 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 index 4975236..713ee32 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/SignatureVerifier.scala @@ -1,7 +1,6 @@ package gs.crypto.v0 -/** - * Used to verify cryptographic signatures. +/** Used to verify cryptographic signatures. */ trait SignatureVerifier[F[_]]: def verify(signature: Signature): 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 index 873d223..3b387f3 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/Signer.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/Signer.scala @@ -3,23 +3,28 @@ package gs.crypto.v0 import java.nio.charset.Charset import java.nio.charset.StandardCharsets -/** - * Used to calculate cryptographic signatures. +/** Used to calculate cryptographic signatures. */ trait Signer[F[_]]: - /** - * Calculate a signature for the given data. - * - * @param data The input data. - * @return The calculated signature. - */ + /** 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] + /** 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 index e9251c7..eb458c7 100644 --- a/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala +++ b/modules/core/src/main/scala/gs/crypto/v0/StringKeyLoader.scala @@ -2,17 +2,18 @@ package gs.crypto.v0 import cats.Applicative -/** - * Implementation of [[KeyLoader]] that loads keys directly from input strings. +/** Implementation of [[KeyLoader]] that loads keys directly from input strings. * - * @param config The key loader configuration. + * @param config + * The key loader configuration. */ class StringKeyLoader[F[_]: Applicative]( val config: BaseKeyLoader.Config ) extends BaseKeyLoader[F]: - /** @inheritDocs */ + /** @inheritDocs + */ override def loadKey( location: String - ): F[Either[KeyLoadError, Array[Byte]]] = + ): F[Either[KeyLoadError, Array[Byte]]] = Applicative[F].pure(prepareKey(location)) 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 268c117..b10cfbe 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 @@ -1,26 +1,27 @@ package gs.crypto.v0.eddsa import cats.effect.Sync -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters 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 -/** - * Implementation of [[Signer]] that uses the Ed25519 system to calculate +/** Implementation of [[Signer]] that uses the Ed25519 system to calculate * signatures. * * See: [[Ed25519Verifier]] * - * @param privateKey The private key. + * @param privateKey + * The private key. */ final class Ed25519Signer[F[_]: Sync]( privateKey: Array[Byte] ) extends Signer[F]: private lazy val params = new Ed25519PrivateKeyParameters(privateKey) - /** @inheritDocs */ + /** @inheritDocs + */ override def sign(data: Array[Byte]): F[Signature] = Sync[F].delay { val s = new org.bouncycastle.crypto.signers.Ed25519Signer() @@ -29,9 +30,10 @@ final class Ed25519Signer[F[_]: Sync]( Signature(s.generateSignature()) } - /** @inheritDocs */ + /** @inheritDocs + */ override def sign( - data: String, + data: String, charset: Charset = StandardCharsets.UTF_8 - ): F[Signature] = + ): 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 index 3057e85..5a00dd4 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 @@ -1,26 +1,27 @@ package gs.crypto.v0.eddsa import cats.effect.Sync -import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import gs.crypto.v0.Signature -import gs.crypto.v0.SignatureVerifier import gs.crypto.v0.SignatureValidity +import gs.crypto.v0.SignatureVerifier +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters -/** - * Implementation of [[SignatureVerifier]] that uses the Ed25519 system to +/** Implementation of [[SignatureVerifier]] that uses the Ed25519 system to * verify signatures. * * See: [[Ed25519Signer]] * - * @param publicKey The public key. + * @param publicKey + * The public key. */ final class Ed25519Verifier[F[_]: Sync]( publicKey: Array[Byte] ) extends SignatureVerifier[F]: private lazy val params = new Ed25519PublicKeyParameters(publicKey) - /** @inheritDocs */ - override def verify(signature: Signature): F[SignatureValidity] = + /** @inheritDocs + */ + override def verify(signature: Signature): F[SignatureValidity] = Sync[F].delay { val s = new org.bouncycastle.crypto.signers.Ed25519Signer() val _ = s.init(false, params) -- 2.43.0 From fae965fa3282de7f623921cc2ccf8c861068c0ea Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Wed, 25 Feb 2026 21:50:22 -0600 Subject: [PATCH 3/4] WIP cleanup but working well --- build.sbt | 6 +- .../scala/gs/crypto/v0/BaseKeyLoader.scala | 35 +++++++++-- .../scala/gs/crypto/v0/FileKeyLoader.scala | 6 +- .../main/scala/gs/crypto/v0/KeyLoader.scala | 47 ++++++++++---- .../gs/crypto/v0/SignatureVerifier.scala | 6 +- .../scala/gs/crypto/v0/StringKeyLoader.scala | 12 ++-- .../scala/gs/crypto/v0/eddsa/Ed25519.scala | 7 +++ .../gs/crypto/v0/eddsa/Ed25519Signer.scala | 13 ++-- .../gs/crypto/v0/eddsa/Ed25519Verifier.scala | 17 +++--- .../gs/crypto/v0/eddsa/Ed25519Tests.scala | 61 +++++++++++++++++++ 10 files changed, 165 insertions(+), 45 deletions(-) create mode 100644 modules/eddsa/src/main/scala/gs/crypto/v0/eddsa/Ed25519.scala create mode 100644 modules/eddsa/src/test/scala/gs/crypto/v0/eddsa/Ed25519Tests.scala 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 -- 2.43.0 From 20702f4af54132697439c65fff6aa2547cd655df Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 15 Mar 2026 22:22:44 -0500 Subject: [PATCH 4/4] (patch) eddsa --- build.sbt | 2 +- .../src/main/scala/gs/crypto/v0/eddsa/Ed25519Verifier.scala | 2 +- project/build.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 77b9139..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") 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 c054aea..2107e0f 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 @@ -22,7 +22,7 @@ final class Ed25519Verifier[F[_]: Sync]( */ override def verify( signature: Signature, - data: Array[Byte] // TODO: CLEAN UP + data: Array[Byte] ): F[SignatureValidity] = Sync[F].delay { val s = java.security.Signature.getInstance(Ed25519.Algorithm) 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 -- 2.43.0