Skip to content

Instantly share code, notes, and snippets.

@johndpope
Forked from elmyn/AWSS3RequestSigner
Created July 2, 2019 07:09
Show Gist options
  • Save johndpope/a4aa53a26afad185b4a4d5d6d60bf3da to your computer and use it in GitHub Desktop.
Save johndpope/a4aa53a26afad185b4a4d5d6d60bf3da to your computer and use it in GitHub Desktop.
Signing AWS GET S3 requests with signature version 4 in Swift 4, Xcode 10
// This is free and unencumbered software released into the public domain.
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
// For more information, please refer to <https://unlicense.org>
//
// AWSS3RequestSigner.swift
// SampleRESTApi
//
// Created by Michal Piwowarczyk on 01.10.2018.
//
import Foundation
import CommonCrypto
//based on:
//https://medium.com/@lewisjkl/signing-aws4-31dcff1bf1f0
//https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
//https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
class AWSS3RequestSigner: NSObject {
private let hmacShaTypeString = "AWS4-HMAC-SHA256"
private let awsRegion = "eu-central-1"
private let serviceType = "s3"
private let aws4Request = "aws4_request"
private let iso8601Formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_GB")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyyMMdd'T'HHmmssXXXXX"
return formatter
}()
private func iso8601() -> (full: String, short: String) {
let date = iso8601Formatter.string(from: Date())
let index = date.index(date.startIndex, offsetBy: 8)
let shortDate = String(date[..<index])
return (full: date, short: shortDate)
}
func signGET(request: URLRequest, secretSigningKey: String, accessKeyId: String) -> URLRequest? {
var signedRequest = request
let date = iso8601()
guard let url = signedRequest.url, let host = url.host else { return nil }
signedRequest.addValue(host, forHTTPHeaderField: "Host")
signedRequest.addValue(date.full, forHTTPHeaderField: "X-Amz-Date")
signedRequest.addValue("".sha256(), forHTTPHeaderField: "x-amz-content-sha256")
signedRequest.addValue("", forHTTPHeaderField: "Range")
guard let headers = signedRequest.allHTTPHeaderFields, let method = signedRequest.httpMethod
else { return nil }
let signedHeaders = headers.map{ $0.key.lowercased() }.sorted().joined(separator: ";")
//If there is no payload in the request, x-amz-content-sha256 should be empty string hashed with sha256
//If its GET method there should be empty line under x-amz-date
let canonicalRequest = """
\(method)
\(url.path)
\(url.query ?? "")
host:\(host)
range:
x-amz-content-sha256:\("".sha256())
x-amz-date:\(date.full)
\(signedHeaders)
\("".sha256())
"""
let canonicalRequestHash = canonicalRequest.sha256()
let credential = getCredential(date: date.short, accessKeyId: accessKeyId)
let stringToSign = [hmacShaTypeString, date.full, credential, canonicalRequestHash].joined(separator: "\n")
guard let signature = signatureWith(stringToSign: stringToSign, secretAccessKey: secretSigningKey, shortDateString: date.short)
else { return nil }
let authorization = hmacShaTypeString + " Credential=" + accessKeyId + "/" + credential + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
signedRequest.addValue(authorization, forHTTPHeaderField: "Authorization")
return signedRequest
}
private func getCredential(date: String, accessKeyId: String) -> String {
let credential = [date, awsRegion, serviceType, aws4Request].joined(separator: "/")
return credential
}
/*
DateKey = HMAC-SHA256("AWS4"+"<SecretAccessKey>", "<YYYYMMDD>")
DateRegionKey = HMAC-SHA256(<DateKey>, "<aws-region>")
DateRegionServiceKey = HMAC-SHA256(<DateRegionKey>, "<aws-service>")
SigningKey = HMAC-SHA256(<DateRegionServiceKey>, "aws4_request")
*/
private func signatureWith(stringToSign: String, secretAccessKey: String, shortDateString: String) -> String? {
let firstKey = "AWS4" + secretAccessKey
let dateKey = shortDateString.hmac(keyString: firstKey)
let dateRegionKey = awsRegion.hmac(keyData: dateKey)
let dateRegionServiceKey = serviceType.hmac(keyData: dateRegionKey)
let signingKey = aws4Request.hmac(keyData: dateRegionServiceKey)
let signature = stringToSign.hmac(keyData: signingKey)
return signature.toHexString()
}
}
private extension String {
func sha256() -> String {
guard let data = self.data(using: .utf8) else { return "" }
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0, CC_LONG(data.count), &hash)
}
let outputData = Data(bytes: hash)
return outputData.toHexString()
}
func hmac(keyString: String) -> Data {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyString, keyString.count, self, self.count, &digest)
let data = Data(bytes: digest)
return data
}
func hmac(keyData: Data) -> Data {
let keyBytes = keyData.bytes()
let data = self.cString(using: String.Encoding.utf8)
let dataLen = Int(self.lengthOfBytes(using: String.Encoding.utf8))
var result = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes, keyData.count, data, dataLen, &result);
return Data(bytes: result)
}
}
private extension Data {
func toHexString() -> String {
let hexString = self.map{ String(format:"%02x", $0) }.joined()
return hexString
}
func bytes() -> [UInt8] {
let array = [UInt8](self)
return array
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment