Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
50f054d
Create authentication plugin project
takapi327 Dec 17, 2025
4d11608
Create AuthenticationPlugin
takapi327 Dec 17, 2025
404e7a1
Create PluginName
takapi327 Dec 17, 2025
0519580
Create MysqlClearPasswordPlugin
takapi327 Dec 17, 2025
604c33f
Create EncryptPasswordPlugin
takapi327 Dec 17, 2025
df73764
Delete unused Authentication
takapi327 Dec 17, 2025
f998baf
Change use AuthenticationPlugin, EncryptPasswordPlugin for Sha256Pass…
takapi327 Dec 17, 2025
e675c3b
Change use PluginName
takapi327 Dec 17, 2025
8e39800
Change use PluginName
takapi327 Dec 17, 2025
1097123
Change use PluginName for MysqlNativePasswordPlugin
takapi327 Dec 17, 2025
065d703
Change to deprecated AuthenticationPlugin, MysqlClearPasswordPlugin
takapi327 Dec 17, 2025
8f5f619
Added plugins property
takapi327 Dec 17, 2025
63e8a26
Added plugins property
takapi327 Dec 17, 2025
015b2ce
Fixed test compile
takapi327 Dec 17, 2025
db1f29b
Delete unused
takapi327 Dec 17, 2025
661983f
Action sbt scalafmtAll
takapi327 Dec 17, 2025
2777604
Action sbt scalafmtSbt
takapi327 Dec 17, 2025
6fd1d8f
Action sbt githubWorkflowGenerate
takapi327 Dec 17, 2025
40f67e6
Create AwsIamAuthenticationPlugin
takapi327 Dec 21, 2025
490446f
Action sbt scalafmtAll
takapi327 Dec 21, 2025
afbfb29
Delete unused
takapi327 Dec 21, 2025
dd0e410
Delete setPlugins method
takapi327 Dec 21, 2025
b00db31
Create aws iam authentication plugin example project
takapi327 Dec 21, 2025
46c99fc
Added healthcheck api
takapi327 Dec 21, 2025
d0a7fe5
Action sbt scalafmtSbt
takapi327 Dec 21, 2025
7df9ebf
Fixed compile error
takapi327 Dec 21, 2025
6db67a9
Action sbt scalafmtAll
takapi327 Dec 21, 2025
4f1b09d
Action sbt githubWorkflowGenerate
takapi327 Dec 21, 2025
00e9b91
Delete unused
takapi327 Dec 21, 2025
127db31
Action sbt scalafmtAll
takapi327 Dec 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
run: mkdir -p module/ldbc-query-builder/.js/target module/ldbc-codegen/native/target module/jdbc-connector/.jvm/target module/ldbc-query-builder/.native/target module/ldbc-codegen/jvm/target module/ldbc-query-builder/.jvm/target module/ldbc-dsl/.native/target module/ldbc-connector/js/target module/ldbc-codegen/js/target module/ldbc-zio-interop/.jvm/target module/ldbc-core/.native/target module/ldbc-sql/.js/target module/ldbc-aws-authentication-plugin/jvm/target module/ldbc-statement/.native/target module/ldbc-core/.js/target module/ldbc-schema/.js/target module/ldbc-sql/.native/target module/ldbc-zio-interop/.js/target module/ldbc-schema/.native/target module/ldbc-statement/.jvm/target module/ldbc-core/.jvm/target module/ldbc-dsl/.js/target module/ldbc-sql/.jvm/target module/ldbc-statement/.js/target module/ldbc-connector/native/target module/ldbc-connector/jvm/target module/ldbc-schema/.jvm/target plugin/target module/ldbc-aws-authentication-plugin/native/target module/ldbc-dsl/.jvm/target module/ldbc-aws-authentication-plugin/js/target project/target
run: mkdir -p module/ldbc-query-builder/.js/target module/ldbc-codegen/native/target module/jdbc-connector/.jvm/target module/ldbc-authentication-plugin/native/target module/ldbc-query-builder/.native/target module/ldbc-codegen/jvm/target module/ldbc-query-builder/.jvm/target module/ldbc-dsl/.native/target module/ldbc-connector/js/target module/ldbc-codegen/js/target module/ldbc-zio-interop/.jvm/target module/ldbc-core/.native/target module/ldbc-sql/.js/target module/ldbc-authentication-plugin/js/target module/ldbc-aws-authentication-plugin/jvm/target module/ldbc-statement/.native/target module/ldbc-core/.js/target module/ldbc-schema/.js/target module/ldbc-sql/.native/target module/ldbc-zio-interop/.js/target module/ldbc-schema/.native/target module/ldbc-statement/.jvm/target module/ldbc-core/.jvm/target module/ldbc-dsl/.js/target module/ldbc-sql/.jvm/target module/ldbc-authentication-plugin/jvm/target module/ldbc-statement/.js/target module/ldbc-connector/native/target module/ldbc-connector/jvm/target module/ldbc-schema/.jvm/target plugin/target module/ldbc-aws-authentication-plugin/native/target module/ldbc-dsl/.jvm/target module/ldbc-aws-authentication-plugin/js/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
run: tar cf targets.tar module/ldbc-query-builder/.js/target module/ldbc-codegen/native/target module/jdbc-connector/.jvm/target module/ldbc-query-builder/.native/target module/ldbc-codegen/jvm/target module/ldbc-query-builder/.jvm/target module/ldbc-dsl/.native/target module/ldbc-connector/js/target module/ldbc-codegen/js/target module/ldbc-zio-interop/.jvm/target module/ldbc-core/.native/target module/ldbc-sql/.js/target module/ldbc-aws-authentication-plugin/jvm/target module/ldbc-statement/.native/target module/ldbc-core/.js/target module/ldbc-schema/.js/target module/ldbc-sql/.native/target module/ldbc-zio-interop/.js/target module/ldbc-schema/.native/target module/ldbc-statement/.jvm/target module/ldbc-core/.jvm/target module/ldbc-dsl/.js/target module/ldbc-sql/.jvm/target module/ldbc-statement/.js/target module/ldbc-connector/native/target module/ldbc-connector/jvm/target module/ldbc-schema/.jvm/target plugin/target module/ldbc-aws-authentication-plugin/native/target module/ldbc-dsl/.jvm/target module/ldbc-aws-authentication-plugin/js/target project/target
run: tar cf targets.tar module/ldbc-query-builder/.js/target module/ldbc-codegen/native/target module/jdbc-connector/.jvm/target module/ldbc-authentication-plugin/native/target module/ldbc-query-builder/.native/target module/ldbc-codegen/jvm/target module/ldbc-query-builder/.jvm/target module/ldbc-dsl/.native/target module/ldbc-connector/js/target module/ldbc-codegen/js/target module/ldbc-zio-interop/.jvm/target module/ldbc-core/.native/target module/ldbc-sql/.js/target module/ldbc-authentication-plugin/js/target module/ldbc-aws-authentication-plugin/jvm/target module/ldbc-statement/.native/target module/ldbc-core/.js/target module/ldbc-schema/.js/target module/ldbc-sql/.native/target module/ldbc-zio-interop/.js/target module/ldbc-schema/.native/target module/ldbc-statement/.jvm/target module/ldbc-core/.jvm/target module/ldbc-dsl/.js/target module/ldbc-sql/.jvm/target module/ldbc-authentication-plugin/jvm/target module/ldbc-statement/.js/target module/ldbc-connector/native/target module/ldbc-connector/jvm/target module/ldbc-schema/.jvm/target plugin/target module/ldbc-aws-authentication-plugin/native/target module/ldbc-dsl/.jvm/target module/ldbc-aws-authentication-plugin/js/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
Expand Down Expand Up @@ -394,7 +394,7 @@ jobs:
- name: Submit Dependencies
uses: scalacenter/sbt-dependency-submission@v2
with:
modules-ignore: ldbcjs_3 ldbcjs_3 otel_3 mcp-ldbc-document-server_sjs1_3 docs_3 docs_3 zio_3 ldbcnative_3 ldbcnative_3 ldbcjvm_3 ldbcjvm_3 hikaricp_3 tests_sjs1_3 tests_sjs1_3 http4s_3 tests_3 tests_3 benchmark_3 benchmark_3 tests_native0.4_3 tests_native0.4_3
modules-ignore: ldbcjs_3 ldbcjs_3 otel_3 mcp-ldbc-document-server_sjs1_3 docs_3 docs_3 zio_3 ldbcnative_3 ldbcnative_3 ldbcjvm_3 ldbcjvm_3 hikaricp_3 tests_sjs1_3 tests_sjs1_3 http4s_3 aws-iam-auth_3 tests_3 tests_3 benchmark_3 benchmark_3 tests_native0.4_3 tests_native0.4_3
configs-ignore: test scala-tool scala-doc-tool test-internal

validate-steward:
Expand Down
36 changes: 34 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ lazy val jdbcConnector = crossProject(JVMPlatform)
.defaultSettings
.dependsOn(core)

lazy val authenticationPlugin = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
.module("authentication-plugin", "MySQL authentication plugin written in pure Scala3")
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % "2.10.0",
"org.scodec" %%% "scodec-bits" % "1.1.38"
)
)
.jsSettings(
Test / scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule))
)
.nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
.nativeSettings(Test / nativeBrewFormulas += "s2n")

lazy val connector = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
.module("connector", "MySQL connector written in pure Scala3")
Expand All @@ -141,7 +156,7 @@ lazy val connector = crossProject(JVMPlatform, JSPlatform, NativePlatform)
)
.nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
.nativeSettings(Test / nativeBrewFormulas += "s2n")
.dependsOn(core)
.dependsOn(core, authenticationPlugin)

lazy val awsAuthenticationPlugin = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
Expand All @@ -159,6 +174,7 @@ lazy val awsAuthenticationPlugin = crossProject(JVMPlatform, JSPlatform, NativeP
)
.nativeEnablePlugins(ScalaNativeBrewedConfigPlugin)
.nativeSettings(Test / nativeBrewFormulas += "s2n")
.dependsOn(authenticationPlugin)

lazy val plugin = LepusSbtPluginProject("ldbc-plugin", "plugin")
.settings(description := "Projects that provide sbt plug-ins")
Expand Down Expand Up @@ -300,6 +316,20 @@ lazy val zioExample = crossProject(JVMPlatform)
)
.dependsOn(connector, dsl, zioInterop)

lazy val awsIamAuthExample = crossProject(JVMPlatform)
.crossType(CrossType.Pure)
.withoutSuffixFor(JVMPlatform)
.example("aws-iam-auth", "Aws Iam Authentication example project")
.settings(
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % "0.23.33",
"org.http4s" %% "http4s-ember-server" % "0.23.33",
"org.http4s" %% "http4s-circe" % "0.23.33",
"io.circe" %% "circe-generic" % "0.14.10"
)
)
.dependsOn(connector, awsAuthenticationPlugin, dsl)

lazy val docs = (project in file("docs"))
.settings(
description := "Documentation for ldbc",
Expand Down Expand Up @@ -401,7 +431,8 @@ lazy val examples = Seq(
http4sExample,
hikariCPExample,
otelExample,
zioExample
zioExample,
awsIamAuthExample
)

lazy val ldbc = tlCrossRootProject
Expand All @@ -418,6 +449,7 @@ lazy val ldbc = tlCrossRootProject
schema,
codegen,
zioInterop,
authenticationPlugin,
awsAuthenticationPlugin,
plugin,
tests,
Expand Down
97 changes: 97 additions & 0 deletions examples/aws-iam-auth/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2023-2025 by Takahiko Tominaga
* This software is licensed under the MIT License (MIT).
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

import com.comcast.ip4s.*

import cats.effect.*
import cats.effect.std.{ Console, Env }

import io.circe.*
import io.circe.syntax.*

import ldbc.dsl.*

import ldbc.connector.*

import ldbc.amazon.plugin.AwsIamAuthenticationPlugin
import ldbc.logging.*

import org.http4s.*
import org.http4s.circe.CirceEntityEncoder.*
import org.http4s.dsl.io.*
import org.http4s.ember.server.EmberServerBuilder

case class City(
id: Int,
name: String,
countryCode: String,
district: String,
population: Int
)

object City:

given Encoder[City] = Encoder.derived[City]

object Main extends ResourceApp.Forever:

private val logHandler: LogHandler[IO] = {
case LogEvent.Success(sql, args) =>
Console[IO].println(
s"""Successful Statement Execution:
| $sql
|
| arguments = [${ args.mkString(",") }]
|""".stripMargin
)
case LogEvent.ProcessingFailure(sql, args, failure) =>
Console[IO].errorln(
s"""Failed ResultSet Processing:
| $sql
|
| arguments = [${ args.mkString(",") }]
|""".stripMargin
) >> Console[IO].printStackTrace(failure)
case LogEvent.ExecFailure(sql, args, failure) =>
Console[IO].errorln(
s"""Failed Statement Execution:
| $sql
|
| arguments = [${ args.mkString(",") }]
|""".stripMargin
) >> Console[IO].printStackTrace(failure)
}

private def routes(connector: Connector[IO]): HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "healthcheck" => Ok("Healthcheck")
case GET -> Root / "cities" =>
for
cities <- sql"SELECT * FROM city".query[City].to[List].readOnly(connector)
result <- Ok(cities.asJson)
yield result
}

override def run(args: List[String]): Resource[IO, Unit] =
for
hostname <- Resource.eval(Env[IO].get("AURORA_HOST").flatMap {
case Some(v) => IO.pure(v)
case None => IO.raiseError(new RuntimeException("AURORA_HOST is not set"))
})
username <- Resource.eval(Env[IO].get("AURORA_USER").flatMap {
case Some(v) => IO.pure(v)
case None => IO.raiseError(new RuntimeException("AURORA_USER is not set"))
})
config = MySQLConfig.default.setHost(hostname).setUser(username).setDatabase("world").setSSL(SSL.Trusted)
plugin = AwsIamAuthenticationPlugin.default[IO]("ap-northeast-1", hostname, username)
datasource <- MySQLDataSource.pooling[IO](config, plugins = List(plugin))
connector = Connector.fromDataSource(datasource, Some(logHandler))
_ <- EmberServerBuilder
.default[IO]
.withHost(host"0.0.0.0")
.withPort(port"9000")
.withHttpApp(routes(connector).orNotFound)
.build
yield ()
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

package ldbc.connector.authenticator
package ldbc.authentication.plugin

import java.nio.charset.StandardCharsets

import scala.scalajs.js
import scala.scalajs.js.typedarray.Uint8Array

import scodec.bits.ByteVector
trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>

trait EncryptPasswordPlugin:

private val crypto = js.Dynamic.global.require("crypto")

def transformation: String

private def xorString(from: Array[Byte], scramble: Array[Byte], length: Int): Array[Byte] =
val scrambleLength = scramble.length
(0 until length).map(pos => (from(pos) ^ scramble(pos % scrambleLength)).toByte).toArray

def encryptPassword(password: String, scramble: Array[Byte], publicKeyString: String): Array[Byte] =
val input = if password.nonEmpty then (password + "\u0000").getBytes(StandardCharsets.UTF_8) else Array[Byte](0)
val mysqlScrambleBuff = xorString(input, scramble, input.length)
Expand All @@ -28,4 +37,3 @@ trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>
ByteVector(input).toUint8Array
)
ByteVector.view(encrypted.asInstanceOf[Uint8Array]).toArray
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

package ldbc.connector.authenticator
package ldbc.authentication.plugin

import java.nio.charset.StandardCharsets
import java.security.interfaces.RSAPublicKey
Expand All @@ -15,7 +15,10 @@ import java.util.Base64

import javax.crypto.Cipher

trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>
trait EncryptPasswordPlugin:

def transformation: String

def encryptPassword(password: String, scramble: Array[Byte], publicKeyString: String): Array[Byte] =
val input = if password.nonEmpty then (password + "\u0000").getBytes(StandardCharsets.UTF_8) else Array[Byte](0)
val mysqlScrambleBuff = xorString(input, scramble, input.length)
Expand All @@ -24,6 +27,10 @@ trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>
decodeRSAPublicKey(publicKeyString)
)

private def xorString(from: Array[Byte], scramble: Array[Byte], length: Int): Array[Byte] =
val scrambleLength = scramble.length
(0 until length).map(pos => (from(pos) ^ scramble(pos % scrambleLength)).toByte).toArray

private def encryptWithRSAPublicKey(input: Array[Byte], key: PublicKey): Array[Byte] =
val cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.ENCRYPT_MODE, key)
Expand All @@ -36,4 +43,3 @@ trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>
val spec = new X509EncodedKeySpec(certificateData)
val kf = KeyFactory.getInstance("RSA")
kf.generatePublic(spec).asInstanceOf[RSAPublicKey]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

package ldbc.connector.authenticator
package ldbc.authentication.plugin

import java.nio.charset.StandardCharsets

import scala.scalanative.unsafe.*
import scala.scalanative.unsigned.*

import ldbc.connector.authenticator.Openssl.*
import ldbc.authentication.plugin.Openssl.*

trait EncryptPasswordPlugin:

def transformation: String

trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>
def encryptPassword(password: String, scramble: Array[Byte], publicKeyString: String): Array[Byte] =
val input = if password.nonEmpty then (password + "\u0000").getBytes(StandardCharsets.UTF_8) else Array[Byte](0)
val mysqlScrambleBuff = xorString(input, scramble, input.length)
Expand All @@ -21,6 +25,10 @@ trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>
publicKeyString
)

private def xorString(from: Array[Byte], scramble: Array[Byte], length: Int): Array[Byte] =
val scrambleLength = scramble.length
(0 until length).map(pos => (from(pos) ^ scramble(pos % scrambleLength)).toByte).toArray

private def encryptWithRSAPublicKey(input: Array[Byte], publicKey: String): Array[Byte] =
Zone { implicit zone =>
val publicKeyCStr = toCString(publicKey)
Expand Down Expand Up @@ -64,4 +72,3 @@ trait Sha256PasswordPluginPlatform[F[_]] { self: Sha256PasswordPlugin[F] =>

result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

package ldbc.connector.authenticator
package ldbc.authentication.plugin

import scala.scalanative.unsafe.*
import scala.scalanative.unsigned.*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-2025 by Takahiko Tominaga
* This software is licensed under the MIT License (MIT).
* For more information see LICENSE or https://opensource.org/licenses/MIT
*/

package ldbc.authentication.plugin

import scodec.bits.ByteVector

/**
* A trait representing a MySQL authentication plugin for database connections.
*
* This trait defines the contract for various authentication mechanisms supported by MySQL,
* including traditional password-based authentication (mysql_native_password) and
* modern authentication methods like mysql_clear_password for IAM authentication.
*
* Authentication plugins are used during the MySQL handshake process to validate
* client credentials and establish secure database connections.
*
* @tparam F The effect type that wraps the authentication operations
*/
trait AuthenticationPlugin[F[_]]:

/**
* The name of the authentication plugin as recognized by the MySQL server.
*
* Common plugin names include:
* - "mysql_native_password" for traditional SHA1-based password authentication
* - "mysql_clear_password" for plaintext password transmission over SSL
* - "caching_sha2_password" for SHA256-based password authentication
* - "mysql_old_password" for legacy MySQL authentication (deprecated)
*
* @return The plugin name string that identifies this authentication method
*/
def name: PluginName

/**
* Indicates whether this authentication plugin requires a secure (encrypted) connection.
*
* Some authentication plugins, particularly those that transmit passwords in cleartext
* (like mysql_clear_password), require SSL/TLS encryption to ensure data security.
* Traditional hashing-based plugins may optionally use encryption but don't strictly require it.
*
* @return true if SSL/TLS connection is mandatory for this plugin, false otherwise
*/
def requiresConfidentiality: Boolean

/**
* Processes the password according to the authentication plugin's requirements.
*
* Different authentication plugins handle passwords differently:
* - mysql_native_password: Performs SHA1-based hashing with the server's scramble
* - mysql_clear_password: Returns the password as plaintext bytes (requires SSL)
* - caching_sha2_password: Performs SHA256-based hashing with salt
*
* @param password The user's password in plaintext
* @param scramble The random challenge bytes sent by the MySQL server during handshake.
* Used as salt/seed for cryptographic hashing in most authentication methods.
* May be ignored by plugins that don't use server-side challenges.
* @return The processed password data wrapped in the effect type F, ready for transmission
* to the MySQL server during authentication
*/
def hashPassword(password: String, scramble: Array[Byte]): F[ByteVector]
Loading