Compare commits

...

1 commit

Author SHA1 Message Date
4458f6ceff
(minor) update library to depend on gs-std
All checks were successful
/ Build and Test Library Snapshot (pull_request) Successful in 2m38s
2026-05-06 22:46:36 -05:00
18 changed files with 46 additions and 499 deletions

View file

@ -33,6 +33,13 @@ val Deps = new {
} }
val Gs = new { val Gs = new {
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 Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
} }
@ -66,6 +73,9 @@ lazy val core = project
) )
.settings( .settings(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
Deps.Gs.Std.Core,
Deps.Gs.Std.IO,
Deps.Gs.Std.Effect,
Deps.Cats.Effect, Deps.Cats.Effect,
Deps.BouncyCastle.PKIX Deps.BouncyCastle.PKIX
) )

View file

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

View file

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

View file

@ -3,7 +3,7 @@ package gs.crypto.v0.argon2
import cats.Applicative import cats.Applicative
import cats.effect.Sync import cats.effect.Sync
import cats.syntax.all.* import cats.syntax.all.*
import gs.crypto.v0.RandomByteProvider import gs.std.v0.effect.Rng
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@ -101,8 +101,8 @@ object Argon2Secret:
*/ */
def generate[F[_]: Applicative]( def generate[F[_]: Applicative](
size: Int, size: Int,
randomByteProvider: RandomByteProvider[F] rng: Rng[F]
): F[Argon2Secret] = ): F[Argon2Secret] =
randomByteProvider.generateBytes(size).map(new Argon2Secret(_)) rng.nextByteArray(size).map(new Argon2Secret(_))
end Argon2Secret end Argon2Secret

View file

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

View file

@ -1,36 +0,0 @@
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
*/
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)
/** 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)

View file

@ -1,51 +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.
*
* 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
*/
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))
/** 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))

View file

@ -2,6 +2,9 @@ package gs.crypto.v0
import cats.effect.Sync import cats.effect.Sync
import cats.syntax.all.* 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.PrivateKey
import java.security.PublicKey import java.security.PublicKey
@ -39,7 +42,8 @@ abstract class BaseKeyLoader[F[_]: Sync] extends KeyLoader[F]:
protected def prepareKey(base: String): Either[KeyLoadError, Array[Byte]] = protected def prepareKey(base: String): Either[KeyLoadError, Array[Byte]] =
scala.util scala.util
.Try { .Try {
config.decoder.decode(trim(collapse(unwrap(base)))) val staged = trim(collapse(unwrap(base)))
config.decoder.decode(B64(staged))
} }
.toEither .toEither
.left .left
@ -79,7 +83,7 @@ object BaseKeyLoader:
shouldCollapse: Boolean, shouldCollapse: Boolean,
shouldTrim: Boolean, shouldTrim: Boolean,
encodingName: String, encodingName: String,
decoder: Decoder[String] decoder: Decoder[B64]
) )
/** Default configuration values that work in most cases. /** Default configuration values that work in most cases.
@ -114,7 +118,7 @@ object BaseKeyLoader:
shouldCollapse = ShouldCollapse, shouldCollapse = ShouldCollapse,
shouldTrim = ShouldTrim, shouldTrim = ShouldTrim,
encodingName = EncodingName, encodingName = EncodingName,
decoder = s => Base64Decoder.decodeUnsafe(s) decoder = s => Base64Decoder.decode(s)
) )
end Defaults end Defaults

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,159 +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.
*/
/** 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 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
* 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

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

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

@ -1,5 +1,12 @@
package gs.crypto.v0 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. /** Represents a cryptographic signature. Opaque type for an array of bytes.
*/ */
opaque type Signature = Array[Byte] opaque type Signature = Array[Byte]
@ -22,7 +29,7 @@ object Signature:
* @return * @return
* The new signature. * The new signature.
*/ */
def fromEncoded(value: Encoded): Signature = def fromEncodedString(value: EncodedString): Signature =
value.decode() value.decode()
given CanEqual[Signature, Signature] = CanEqual.derived given CanEqual[Signature, Signature] = CanEqual.derived

View file

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

View file

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