eddsa #1

Merged
pfm merged 4 commits from eddsa into main 2026-03-28 03:07:13 +00:00
14 changed files with 204 additions and 177 deletions
Showing only changes of commit f4bbd53d44 - Show all commits

View file

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

View file

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

View file

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

View file

@ -1,23 +1,25 @@
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]]] =

View file

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

View file

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

View file

@ -1,20 +1,21 @@
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]]] =
@ -22,6 +23,4 @@ class ResourceKeyLoader[F[_]: Sync](
.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)))

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,17 @@ 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]]] =

View file

@ -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,7 +30,8 @@ final class Ed25519Signer[F[_]: Sync](
Signature(s.generateSignature())
}
/** @inheritDocs */
/** @inheritDocs
*/
override def sign(
data: String,
charset: Charset = StandardCharsets.UTF_8

View file

@ -1,25 +1,26 @@
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 */
/** @inheritDocs
*/
override def verify(signature: Signature): F[SignatureValidity] =
Sync[F].delay {
val s = new org.bouncycastle.crypto.signers.Ed25519Signer()