Skip to content

Instantly share code, notes, and snippets.

@Daenyth
Last active April 18, 2019 05:11
Show Gist options
  • Save Daenyth/9450c495e2b1064a5bfd35cc2256bdfa to your computer and use it in GitHub Desktop.
Save Daenyth/9450c495e2b1064a5bfd35cc2256bdfa to your computer and use it in GitHub Desktop.
Draft http4s middleware for AWS request signing
// Based on https://github.com/http4s/contrib/blob/master/aws/src/main/scala/org/http4s/contrib/aws/AwsSigner.scala
import java.util.Date
import cats.data.Kleisli
import cats.effect.{Effect, Sync}
import cats.implicits._
import fs2.Stream
import org.http4s.client.Client
import org.http4s.{Header, Request}
import scodec.bits.ByteVector
object AwsSigner {
def apply[F[_]: Effect](id: String,
secret: String,
zone: String,
service: String)(client: Client[F]): Client[F] = {
val signer = new AwsSigner(Key(id, secret), zone, service)
client.copy(open = Kleisli { req =>
signer[F](req, Effect[F].delay(new Date)).flatMap(client.open(_))
})
}
case class Key(id: String, secret: String) {
def bytes =
ByteVector
.fromBase64(secret)
.getOrElse(throw new Exception(s"'$secret' not base64"))
}
}
/** Implements AWS request signing v4 as an http4s middleware.
* Signing described here: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
*/
class AwsSigner(key: AwsSigner.Key, zone: String, service: String) {
val Method = "AWS4-HMAC-SHA256"
val Charset = java.nio.charset.Charset.forName("UTF-8")
private def dateFormat(s: String) = {
val f = new java.text.SimpleDateFormat(s)
f.setTimeZone(java.util.TimeZone.getTimeZone("UTC"))
f
}
val fullDateFormat = dateFormat("YYYYMMdd'T'HHmmss'Z'")
val shortDateFormat = dateFormat("YYYYMMdd")
def hash(bytes: ByteVector) = {
val digest = java.security.MessageDigest.getInstance("SHA-256")
bytes.grouped(1024 * 16) foreach { chunk =>
digest.update(chunk.toByteBuffer)
}
ByteVector(digest.digest)
}
def bytes(s: String) = ByteVector(s.getBytes(Charset))
def hmac(key: ByteVector, data: ByteVector) = {
val algo = "HmacSHA256"
val hmac = javax.crypto.Mac.getInstance(algo)
hmac.init(new javax.crypto.spec.SecretKeySpec(key.toArray, algo))
ByteVector(hmac.doFinal(data.toArray))
}
def sign(string: String, date: java.util.Date) = {
val kSecret = bytes(s"AWS4${key.secret}")
val kDate = hmac(kSecret, bytes(shortDateFormat.format(date)))
val kRegion = hmac(kDate, bytes(zone))
val kService = hmac(kRegion, bytes(service))
val kSigning = hmac(kService, bytes("aws4_request"))
hmac(kSigning, bytes(string))
}
def apply[F[_]: Effect](request: Request[F],
getDate: F[Date]): F[Request[F]] = {
request.body.compile.to[Array].flatMap { fullBodyArray =>
getDate.map { date =>
val fullBody = ByteVector(fullBodyArray)
val amzDate = Header("x-amz-date", fullDateFormat.format(date))
val amzHost = Header("Host",
request.uri.host
.map(_.toString)
.getOrElse(throw new Exception("need a Host")))
val headersToSign = request.headers
.put(amzDate)
.put(amzHost)
.toList sortBy { h =>
h.name.toString.toLowerCase
}
val signedHeaders = headersToSign
.map(header => header.name.toString.toLowerCase)
.mkString(";")
val canonicalRequest = Seq(
request.method.name,
request.uri.path,
request.queryString,
headersToSign
.map({ header =>
s"${header.name.toString.toLowerCase}:${header.value.trim}\n"
})
.mkString(""),
signedHeaders,
hash(fullBody).toHex
) mkString "\n"
val stringToSign = Seq(
Method,
fullDateFormat.format(date),
shortDateFormat.format(date) + s"/$zone/$service/aws4_request",
hash(ByteVector(canonicalRequest.getBytes(Charset))).toHex
) mkString "\n"
val auth = Seq(
"Credential" -> s"${key.id}/${shortDateFormat.format(date)}/$zone/$service/aws4_request",
"SignedHeaders" -> signedHeaders,
"Signature" -> sign(stringToSign, date).toHex
) map { case (k, v) => s"$k=$v" } mkString ", "
request
.putHeaders(Header("Authorization", s"$Method $auth"),
amzDate,
amzHost)
.withBodyStream(Stream.emits(fullBodyArray))
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment