Compare commits

...

2 commits
0.1.1 ... main

Author SHA1 Message Date
700fab5c41 (minor) update library to depend on gs-std (#2)
All checks were successful
/ Build and Release Library (push) Successful in 2m42s
Reviewed-on: #2
2026-05-07 03:54:10 +00:00
3e1a2467ae (minor) eddsa
All checks were successful
/ Build and Release Library (push) Successful in 2m42s
Reviewed-on: #1
2026-03-28 03:07:12 +00:00
33 changed files with 622 additions and 412 deletions

View file

@ -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,19 @@ val Deps = new {
}
val Gs = new {
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.1.1"
val Std = new {
val Version: String = "0.1.2"
val Core: ModuleID = "gs" %% "gs-std-core-v0" % Version
val IO: ModuleID = "gs" %% "gs-std-io-v0" % Version
val Effect: ModuleID = "gs" %% "gs-std-effect-v0" % Version
}
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 +60,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 +73,11 @@ lazy val core = project
)
.settings(
libraryDependencies ++= Seq(
Deps.Cats.Effect
Deps.Gs.Std.Core,
Deps.Gs.Std.IO,
Deps.Gs.Std.Effect,
Deps.Cats.Effect,
Deps.BouncyCastle.PKIX
)
)
@ -97,3 +109,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
)
)

View file

@ -2,7 +2,7 @@ package gs.crypto.v0.argon2
import cats.effect.Sync
import cats.syntax.all.*
import gs.crypto.v0.RandomByteProvider
import gs.std.v0.effect.Rng
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
@ -20,7 +20,7 @@ import org.bouncycastle.crypto.params.Argon2Parameters
final class Argon2[F[_]: Sync](
val config: Argon2.Config,
val secret: Argon2Secret,
val randomByteProvider: RandomByteProvider[F]
val rng: Rng[F]
):
/** Calculate a new hash for some input.
@ -82,7 +82,7 @@ final class Argon2[F[_]: Sync](
}
private def randomSalt(): F[Array[Byte]] =
randomByteProvider.generateBytes(config.saltLengthInBytes)
rng.nextByteArray(config.saltLengthInBytes)
private def buildAlgorithmParams(salt: Array[Byte]): Argon2Parameters =
new Argon2Parameters.Builder(config.algorithmType)
@ -137,7 +137,7 @@ object Argon2:
)
/** @return
* [[Argon2.Params]] with default settings. Suitable for most cases.
* [[Argon2.Config]] with default settings. Suitable for most cases.
*/
def defaultConfig(): Config =
Config(

View file

@ -1,7 +1,7 @@
package gs.crypto.v0.argon2
import gs.crypto.v0.B64
import gs.crypto.v0.Base64Encoder
import gs.std.v0.core.B64
import gs.std.v0.core.Base64Encoder
import java.util.Base64
import java.util.Objects
import scala.util.Try

View file

@ -3,7 +3,7 @@ package gs.crypto.v0.argon2
import cats.Applicative
import cats.effect.Sync
import cats.syntax.all.*
import gs.crypto.v0.RandomByteProvider
import gs.std.v0.effect.Rng
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
@ -94,15 +94,15 @@ object Argon2Secret:
*
* @param size
* The number of bytes in the secret value.
* @param randomByteProvider
* The [[RandomByteProvider]].
* @param rng
* The random number generator.
* @return
* The new [[Argon2Secret]].
*/
def generate[F[_]: Applicative](
size: Int,
randomByteProvider: RandomByteProvider[F]
rng: Rng[F]
): F[Argon2Secret] =
randomByteProvider.generateBytes(size).map(new Argon2Secret(_))
rng.nextByteArray(size).map(new Argon2Secret(_))
end Argon2Secret

View file

@ -2,7 +2,7 @@ package gs.crypto.v0.argon2
import cats.effect.IO
import cats.effect.unsafe.IORuntime
import gs.crypto.v0.RandomByteProvider
import gs.std.v0.effect.Rng
import munit.Location
import org.bouncycastle.crypto.params.Argon2Parameters
@ -19,7 +19,7 @@ class Argon2Tests extends munit.FunSuite:
): Unit =
test(name)(f.unsafeRunSync())
val rng: RandomByteProvider[IO] = RandomByteProvider.secureRandom[IO]
val rng: Rng[IO] = Rng.secure[IO]
val altConfig: Argon2.Config = Argon2.Config(
algorithmVersion = Argon2Parameters.ARGON2_VERSION_10,

View file

@ -1,23 +0,0 @@
package gs.crypto.v0
import java.{util => ju}
/** Implementation of [[Decoder]] for Base64 strings.
*/
object Base64Decoder extends Decoder[B64]:
private lazy val d: ju.Base64.Decoder = ju.Base64.getDecoder()
/** @inheritDocs
*/
override def decode(input: B64): Array[Byte] =
d.decode(input.data)
/** Decode some arbitrary string data.
*
* @param input
* The data to decode.
* @return
* The decoded bytes.
*/
def decodeUnsafe(input: String): Array[Byte] =
d.decode(input)

View file

@ -1,23 +0,0 @@
package gs.crypto.v0
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.Base64
/** Implementation of [[Encoder]] for Base64.
*/
object Base64Encoder extends Encoder[B64]:
private lazy val e: Base64.Encoder = Base64.getEncoder()
/** @inheritDocs
*/
override def encode(input: Array[Byte]): B64 =
B64(e.encodeToString(input))
/** @inheritDocs
*/
override def encode(
input: String,
charset: Charset = StandardCharsets.UTF_8
): B64 =
encode(input.getBytes(charset))

View file

@ -0,0 +1,126 @@
package gs.crypto.v0
import cats.effect.Sync
import cats.syntax.all.*
import gs.std.v0.core.B64
import gs.std.v0.core.Base64Decoder
import gs.std.v0.core.Decoder
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 {
val staged = trim(collapse(unwrap(base)))
config.decoder.decode(B64(staged))
}
.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[B64]
)
/** 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.decode(s)
)
end Defaults
end BaseKeyLoader

View file

@ -1,4 +0,0 @@
package gs.crypto.v0
trait Decoder[A]:
def decode(input: A): Array[Byte]

View file

@ -1,58 +0,0 @@
package gs.crypto.v0
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
/** Helper functions for encoding data.
*/
object Encode:
/** Encode an array of bytes using base64.
*
* @param input
* The bytes to encode.
* @return
* The base64 string.
*/
def base64(input: Array[Byte]): B64 = Base64Encoder.encode(input)
/** Encode a string using base64.
*
* @param input
* The string to encode.
* @param charset
* The character set of the input string.
* @return
* The base64 string.
*/
def base64(
input: String,
charset: Charset = StandardCharsets.UTF_8
): B64 =
Base64Encoder.encode(input, charset)
/** Encode an array of bytes using hexadecimal.
*
* @param input
* The bytes to encode.
* @return
* The hexadecimal string.
*/
def hex(input: Array[Byte]): Hex = HexEncoder.encode(input)
/** Encode a string using hexadecimal.
*
* @param input
* The string to encode.
* @param charset
* The character set of the input string.
* @return
* The hexadecimal string.
*/
def hex(
input: String,
charset: Charset = StandardCharsets.UTF_8
): Hex =
HexEncoder.encode(input, charset)
end Encode

View file

@ -1,109 +0,0 @@
package gs.crypto.v0
/** Represents encoded data.
*
* See:
* - [[B64]]
* - [[Hex]]
*/
trait Encoded:
/** @return
* The encoded data (expressed as a string).
*/
def data: String
/** @return
* Decode the data to a byte array.
*/
def decode(): Array[Byte]
/** Represents Base64-encoded data.
*
* @param data
* The encoded data.
*/
final class B64(
val data: String
) extends Encoded:
/** @inheritDocs
*/
def decode(): Array[Byte] = Base64Decoder.decode(this)
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: B64 => data == other.data
/** @inheritDocs
*/
override def hashCode(): Int = data.hashCode()
/** @inheritDocs
*/
override def toString(): String = data
object B64:
/** Instantiate [[B64]] from the given string.
*
* This function does NOT validate the input.
*
* @param data
* The encoded data.
* @return
* The new [[B64]] instance.
*/
def apply(
data: String
): B64 = new B64(data)
given CanEqual[B64, B64] = CanEqual.derived
end B64
/** Represents Hex-encoded data.
*
* @param data
* The encoded data.
*/
final class Hex(
val data: String
) extends Encoded:
/** @inheritDocs
*/
def decode(): Array[Byte] = HexDecoder.decode(this)
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: Hex => data == other.data
/** @inheritDocs
*/
override def hashCode(): Int = data.hashCode()
/** @inheritDocs
*/
override def toString(): String = data
object Hex:
/** Instantiate [[Hex]] from the given string.
*
* This function does NOT validate the input.
*
* @param data
* The encoded data.
* @return
* The new [[Hex]] instance.
*/
def apply(
data: String
): Hex = new Hex(data)
given CanEqual[Hex, Hex] = CanEqual.derived
end Hex

View file

@ -1,44 +0,0 @@
package gs.crypto.v0
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
/** Interface for byte encoding to String formats.
*/
trait Encoder[+A <: Encoded]:
/** Encode an array of bytes.
*
* @param input
* The bytes to encode.
* @return
* The encoded string.
*/
def encode(input: Array[Byte]): A
/** Encode a string.
*
* @param input
* The string to encode.
* @param charset
* The character set of the input string.
* @return
* The encoded string.
*/
def encode(
input: String,
charset: Charset = StandardCharsets.UTF_8
): A
object Encoder:
/** @return
* The [[Base64Encoder]], typed to `Encoder[Encoded]`.
*/
def base64(): Encoder[Encoded] = Base64Encoder
/** @return
* The [[HexEncoder]], typed to `Encoder[Encoded]`.
*/
def hex(): Encoder[Encoded] = HexEncoder
end Encoder

View file

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

View file

@ -1,23 +0,0 @@
package gs.crypto.v0
import java.util.HexFormat
/** Implementation of [[Decoder]] for Hexadecimal strings.
*/
object HexDecoder extends Decoder[Hex]:
private lazy val h: HexFormat = HexFormat.of()
/** @inheritDocs
*/
override def decode(input: Hex): Array[Byte] =
h.parseHex(input.data)
/** Decode some arbitrary string data.
*
* @param input
* The data to decode.
* @return
* The decoded bytes.
*/
def decodeUnsafe(input: String): Array[Byte] =
h.parseHex(input)

View file

@ -1,23 +0,0 @@
package gs.crypto.v0
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.HexFormat
/** Implementation of [[Encoder]] for Hexadecimal strings.
*/
object HexEncoder extends Encoder[Hex]:
private lazy val h: HexFormat = HexFormat.of()
/** @inheritDocs
*/
override def encode(input: Array[Byte]): Hex =
Hex(h.formatHex(input))
/** @inheritDocs
*/
override def encode(
input: String,
charset: Charset = StandardCharsets.UTF_8
): Hex =
encode(input.getBytes(charset))

View file

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

View file

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

View file

@ -1,77 +0,0 @@
package gs.crypto.v0
import cats.Applicative
import cats.effect.Sync
import java.security.SecureRandom
/** Utility which produces random bytes. Used for cryptographic support.
*/
trait RandomByteProvider[F[_]]:
/** Generate the specified number of random bytes.
*
* @param numberOfBytes
* The number of bytes to generate.
* @return
* New array with the specified number of random bytes.
*/
def generateBytes(numberOfBytes: Int): F[Array[Byte]]
object RandomByteProvider:
/** Default random number generator.
*/
lazy val RNG: SecureRandom = new SecureRandom
/** New [[RandomByteProvider]] that uses the default (secure) random number
* generator to produce bytes.
*
* @return
* The new provider instance.
*/
def secureRandom[F[_]: Sync]: RandomByteProvider[F] =
new SecureRandomProvider[F](RNG)
/** New [[RandomByteProvider]] that uses the given `SecureRandom` instance to
* produce bytes.
*
* @param random
* The `SecureRandom` instance.
* @return
* The new provider.
*/
def secureRandom[F[_]: Sync](random: SecureRandom): RandomByteProvider[F] =
new SecureRandomProvider[F](random)
/** @return
* New [[RandomByteProvider]] that returns an array of the null byte.
*/
def zero[F[_]: Applicative]: RandomByteProvider[F] =
new Zero[F]
/** Implementation of [[RandomByteProvider]] that uses `SecureRandom`.
*
* @param random
* The random number generator.
*/
final class SecureRandomProvider[F[_]: Sync](
val random: SecureRandom
) extends RandomByteProvider[F]:
/** @inheritDocs
*/
override def generateBytes(numberOfBytes: Int): F[Array[Byte]] =
Sync[F].delay {
val b = new Array[Byte](numberOfBytes)
random.nextBytes(b)
b
}
/** Implementation of [[RandomByteProvider]] that always returns an array of
* the null byte.
*/
final class Zero[F[_]: Applicative] extends RandomByteProvider[F]:
override def generateBytes(numberOfBytes: Int): F[Array[Byte]] =
Applicative[F].pure(Array.fill(numberOfBytes)(0))
end RandomByteProvider

View file

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

View file

@ -0,0 +1,58 @@
package gs.crypto.v0
import gs.std.v0.core.B64
import gs.std.v0.core.B64Url
import gs.std.v0.core.Base64Encoder
import gs.std.v0.core.EncodedString
import gs.std.v0.core.Hex
import gs.std.v0.core.HexEncoder
/** 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 fromEncodedString(value: EncodedString): 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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
package gs.crypto.v0.eddsa
object Ed25519:
val Algorithm: String = "Ed25519"
end Ed25519

View file

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

View file

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

View file

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIPkucDMoZPkyltc200nNyyfJKWP7cu3Tr9gdsFB6PeqN
-----END PRIVATE KEY-----

View file

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAYNI09OpzbZ9GiKhZzNEbwWYISkR2ihfvWEsZx62b+bY=
-----END PUBLIC KEY-----

View file

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

View file

@ -3,9 +3,9 @@ package gs.crypto.v0.rsa
import cats.effect.Sync
import cats.effect.kernel.Resource
import cats.syntax.all.*
import gs.crypto.v0.Base64Decoder
import gs.std.v0.core.Base64Decoder
import gs.std.v0.io.Files
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyFactory
import java.security.PrivateKey
@ -116,8 +116,8 @@ object Rsa:
def loadPublicKeyFromFile[F[_]: Sync](
publicKeyPath: Path
): F[PublicKey] =
Sync[F]
.delay(Files.readString(publicKeyPath, StandardCharsets.UTF_8))
Files
.readFileAsString(publicKeyPath, StandardCharsets.UTF_8)
.map(preparePublicKey)
.flatMap(loadPublicKey[F])
@ -144,8 +144,8 @@ object Rsa:
def loadPrivateKeyFromFile[F[_]: Sync](
privateKeyPath: Path
): F[PrivateKey] =
Sync[F]
.delay(Files.readString(privateKeyPath, StandardCharsets.UTF_8))
Files
.readFileAsString(privateKeyPath, StandardCharsets.UTF_8)
.map(preparePrivateKey)
.flatMap(loadPrivateKey[F])

View file

@ -1,7 +1,7 @@
package gs.crypto.v0.rsa
import gs.crypto.v0.Encoded
import gs.crypto.v0.Encoder
import gs.std.v0.core.EncodedString
import gs.std.v0.core.Encoder
/** Represents arbitrary bytes that were encrypted using RSA.
*
@ -15,7 +15,8 @@ import gs.crypto.v0.Encoder
*/
final class RsaEncryptedBytes(val bytes: Array[Byte]):
def encode(encoder: Encoder[Encoded] = Encoder.base64()): Encoded =
def encode(encoder: Encoder[EncodedString] = Encoder.base64())
: EncodedString =
encoder.encode(bytes)
/** @inheritDocs
@ -39,7 +40,7 @@ object RsaEncryptedBytes:
* @return
* The new [[RsaEncryptedBytes]].
*/
def decode(value: Encoded): RsaEncryptedBytes =
def decode(value: EncodedString): RsaEncryptedBytes =
new RsaEncryptedBytes(value.decode())
end RsaEncryptedBytes

View file

@ -1 +1 @@
sbt.version=1.12.2
sbt.version=1.12.4