From fd7b32a8642bf1819a5d3826505d18e567e3c068 Mon Sep 17 00:00:00 2001 From: Brian Holt Date: Thu, 26 Sep 2024 18:58:30 -0500 Subject: [PATCH] properly decode the ciphertext into a blob using base64 decoding --- build.sbt | 2 ++ .../scala/com/dwolla/config/package.scala | 26 +++++++++++--- .../scala/com/dwolla/config/ExampleApp.scala | 35 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/test/scala/com/dwolla/config/ExampleApp.scala diff --git a/build.sbt b/build.sbt index 7f0c3d7..487f573 100644 --- a/build.sbt +++ b/build.sbt @@ -83,8 +83,10 @@ lazy val `secure-config` = (project in file(".")) "io.monix" %% "newtypes-core" % "0.3.0", "com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value, "com.disneystreaming.smithy4s" %% "smithy4s-aws-http4s" % smithy4sVersion.value, + "org.scodec" %% "scodec-bits" % "1.2.1", "org.typelevel" %% "mouse" % "1.3.2", "org.scalameta" %% "munit" % "1.0.2" % Test, + "org.http4s" %% "http4s-ember-client" % "0.23.28" % Test, ) }, smithy4sAwsSpecs ++= Seq(AWS.kms), diff --git a/src/main/scala/com/dwolla/config/package.scala b/src/main/scala/com/dwolla/config/package.scala index c4390df..5a34a8f 100644 --- a/src/main/scala/com/dwolla/config/package.scala +++ b/src/main/scala/com/dwolla/config/package.scala @@ -7,20 +7,28 @@ import fs2.compression.Compression import monix.newtypes.NewtypeWrapped import mouse.all.* import pureconfig.ConfigReader +import scodec.bits.ByteVector import smithy4s.Blob import smithy4s.aws.{AwsClient, AwsEnvironment} +import scala.util.control.NoStackTrace + package object config { private[this] val secureStringRegex = "^SECURE: (.+)".r def SecureReader[F[_] : Async : Compression](awsEnv: AwsEnvironment[F]): Resource[F, ConfigReader[F[SecurableString]]] = AwsClient(KMS, awsEnv).map { kms => ConfigReader[String].map { - case secureStringRegex(cryptotext) => - kms.decrypt(CiphertextType(Blob(cryptotext.getBytes()))) - .map(_.plaintext) // TODO does this need to be base64-decoded? + case secureStringRegex(ciphertext) => + ByteVector.fromBase64(ciphertext) + .map(_.toArray) + .map(Blob(_)) + .liftTo[F](InvalidCiphertextException(ciphertext)) + .map(CiphertextType(_)) + .flatMap(kms.decrypt(_)) + .map(_.plaintext) .liftOptionT - .getOrRaise(new RuntimeException("boom")) // TODO convert to a better exception + .getOrRaise(UnexpectedMissingPlaintextResponseException) .map(_.value.toUTF8String) .map(SecurableString(_)) @@ -31,3 +39,13 @@ package object config { type SecurableString = SecurableString.Type object SecurableString extends NewtypeWrapped[String] } + +class InvalidCiphertextException(txt: String) + extends RuntimeException(s"The provided ciphertext $txt is invalid, probably because it is not base64 encoded") +object InvalidCiphertextException { + def apply(txt: String): Throwable = new InvalidCiphertextException(txt) +} + +object UnexpectedMissingPlaintextResponseException + extends RuntimeException("the KMS response was expected to contain a plaintext field, but it did not") + with NoStackTrace diff --git a/src/test/scala/com/dwolla/config/ExampleApp.scala b/src/test/scala/com/dwolla/config/ExampleApp.scala new file mode 100644 index 0000000..923d780 --- /dev/null +++ b/src/test/scala/com/dwolla/config/ExampleApp.scala @@ -0,0 +1,35 @@ +package com.dwolla.config + +import cats.effect.* +import fs2.compression.Compression +import fs2.io.file.Files +import fs2.io.net.Network +import org.http4s.ember.client.EmberClientBuilder +import pureconfig.module.catseffect.loadF +import pureconfig.{ConfigReader, ConfigSource} +import smithy4s.aws.{AwsEnvironment, AwsRegion} + +object ExampleApp extends ResourceApp.Simple { + private def reader[F[_] : Async : Compression : Files : Network] = + for { + httpClient <- EmberClientBuilder.default.build + awsEnv <- AwsEnvironment.default[F](httpClient, AwsRegion.US_WEST_2) // TODO get region from environment + secureReader <- SecureReader[F](awsEnv) + } yield secureReader + + override def run: Resource[IO, Unit] = + reader[IO].evalMap { implicit r: ConfigReader[IO[SecurableString]] => + loadF[IO, Foo[IO]](ConfigSource.string( + """ + |foo = "SECURE: AQICAHh38+DAqADvcRLU4+t2AYhr82YbZuuFQdjdX95NTppHhwEQd+ovBiiMlelM0yL+97WRAAAAYTBfBgkqhkiG9w0BBwagUjBQAgEAMEsGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMlNQWVvaAt/VACynHAgEQgB4AzBpBA1ozpJFZTIhC91Q+Emlx40gbhTFmXyqBE+g=" + |""".stripMargin)) + .flatTap(IO.println(_)) + .flatMap(_.foo) + .flatMap(IO.println(_)) + } +} + +case class Foo[F[_]](foo: F[SecurableString]) +object Foo { + implicit def configReader[F[_]](implicit ev: ConfigReader[F[SecurableString]]): ConfigReader[Foo[F]] = ConfigReader.forProduct1("foo")(Foo.apply[F] _) +}