-
-
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 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
// 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