Skip to content

Commit

Permalink
properly decode the ciphertext into a blob using base64 decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
bpholt committed Sep 27, 2024
1 parent 6be954f commit ec1aeee
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 4 deletions.
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] _)
}

0 comments on commit ec1aeee

Please sign in to comment.