Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

properly decode the ciphertext into a blob using base64 decoding #93

Merged
merged 1 commit into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
26 changes: 22 additions & 4 deletions src/main/scala/com/dwolla/config/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(_))

Expand All @@ -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
35 changes: 35 additions & 0 deletions src/test/scala/com/dwolla/config/ExampleApp.scala
Original file line number Diff line number Diff line change
@@ -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

// encrypt some text using `aws kms encrypt --key-id alias/my-key --plaintext foo | jq -r .CiphertextBlob` and replace the base64 text below
val base64CipherText = "AQICAHh38+DAqADvcRLU4+t2AYhr82YbZuuFQdjdX95NTppHhwEQd+ovBiiMlelM0yL+97WRAAAAYTBfBgkqhkiG9w0BBwagUjBQAgEAMEsGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMlNQWVvaAt/VACynHAgEQgB4AzBpBA1ozpJFZTIhC91Q+Emlx40gbhTFmXyqBE+g="

override def run: Resource[IO, Unit] =
reader[IO].evalMap { implicit r: ConfigReader[IO[SecurableString]] =>
loadF[IO, Foo[IO]](ConfigSource.string(s"""foo = "SECURE: $base64CipherText""""))
.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] _)
}