Last active
April 18, 2019 05:11
-
-
Save Daenyth/9450c495e2b1064a5bfd35cc2256bdfa to your computer and use it in GitHub Desktop.
Draft http4s middleware for AWS request signing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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