-
-
Save elmyn/b63913d7ba4ffa26b37d55c7b7e260e1 to your computer and use it in GitHub Desktop.
// 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 | |
} | |
} | |
Hi elmyn,
Thanks for your awesome gist.
Following your guide, I can get signed request worked with IAM user access key and secret key, but running into a 403 issue when using temporary credentials including access, secret and session keys that I got from Cognito authentication.
I tried to add session: String? = nil
parameter to signGet function, and then:
if let session = session {
signedRequest.addValue(session, forHTTPHeaderField: "x-amz-security-token")
}
but the issue is still there.
Have you ever made effort with this before?
Any ideas to resolve this is very much appreciated.
Thanks.
so cognito has user authentication separate to s3 buckets. you can't grant access to a s3 bucket per cognito user that I'm aware of.
UPDATE - @luannguyenkhoa
so what you're describing is - you want to hit your api - and get a pre-signed s3 url back using the cognito credentials....
this is the way - embedding the IAM credentials in app is subject to hacking as anyone could man in the middle attack - and grab keys using charles.
Hi elmyn,
Thanks for the code.
I am using this code to "createmultipartupload", My request is getting success in postman, Where as it not working in code.
Signature is same as postman.
Am getting 403 error.
Is there anything specific which i should use for "POST" request .
Any ideas to resolve this is very much appreciated.
Thanks.
can i get this code with android
I have calling a GET request but getting 403.
For more detail check
It has been a long time to revisit this thread.
I remember managed to make the sign GET request S3 worked with these simple lines of code.
Maybe some code get deprecated by the latest aws ios sdk version, just replace with the new recommendation.
Hope it helps someone stuck in this freaking issue 😄
public func signS3Request(url: URL) -> URLRequest {
let credentials = AWSServiceManager.default()?.defaultServiceConfiguration.credentialsProvider
let signature = AWSSignatureV4Signer(credentialsProvider: credentials!, endpoint: AWSEndpoint(region: .USEast1, service: .S3, url: url))
let date = (NSDate.aws_clockSkewFixed()! as NSDate).aws_stringValue(AWSDateISO8601DateFormat2)!
let req = NSMutableURLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30.0)
req.httpMethod = "GET"
req.allHTTPHeaderFields = ["Content-Type": "application/x-www-form-urlencoded", "X-Amz-Date": date]
signature.interceptRequest(req)?.continueWith(block: { _ in return nil })
return req as URLRequest
}
usage:
` func makeRequest(urlString: String, accessKeyId: String, secretAccessKey: String) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)