iOS로 AWS Request 4 요청을 보내는 게 생각보다 쉽지 않아 정리해보기로 했다.
PUT test$file.text HTTP/1.1
Host: examplebucket.s3.amazonaws.com
Date: Fri, 24 May 2013 00:00:00 GMT
Authorization: SignatureToBeCalculated
x-amz-date: 20130524T000000Z
x-amz-storage-class: REDUCED_REDUNDANCY
x-amz-content-sha256: 44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072
<Payload>
많은 과정이 있지만 결국 해야되는 것은 위와 같이 header를 만들어주는 것이다.
먼저 Date형식부터 위 같이 맞춰줘야 하는데, 이때 Date의 timezone을 UTC시간으로 설정해야한다.
Date는 x-amz-date에 필요한 형식과 다른 timestamp형식이 하나 더 필요한데 이는 index를 이용해서 잘라주었다.
private var dateString:String{
switch self {
case .saveFile:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssXXXXX"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter.string(from: Date())
}
}
private var timestampString:String{
let index = dateString.index(dateString.startIndex, offsetBy: 7)
return String(dateString[...index])
}
PUT연산을 할때는 Payload를 hashing해서, header에 넣어줘야 한다.
private var hashedPayload: String{
switch self {
case .saveFile(let url):
do{
let payload = try Data(contentsOf:url)
return SHA256.hash(data: payload).map{String(format: "%02hhx", $0)}.joined()
}catch{
fatalError("파일 hash 과정 중 오류 발생: " + error.localizedDescription)
}
}
}
그 다음에는 CanonicalRequest를 문자열로 생성해준다.
let canonicalHeaders = """
content-type:application/zip\n\
host:\(host)\n\
x-amz-content-sha256:\(hashedPayload)\n\
x-amz-date:\(dateString)
"""
let signedHeaders = "content-type;host;x-amz-content-sha256;x-amz-date"
let canonicalRequest = "PUT\n/\(endPoint)\n\n\(canonicalHeaders)\n\n\(signedHeaders)\n\(hashedPayload)"
이때 signedHeaders의 순서는 알파벳 순서이다.
stringToSign은 CanonicalRequest를 hashing해서 형식을 맞춰준다.
let credentialScope:String = timestampString + "/" + region + "/" + service + "/" + "aws4_request"
let stringToSign:String = algorithm + "\n" + dateString + "\n" + credentialScope + "\n"
+ SHA256.hash(data: canonicalRequest.data(using: .utf8)!).map{String(format: "%02hhx", $0)}.joined()
let algorithm = "AWS4-HMAC-SHA256" let region = "ap-northeast-2" let service = "s3"
드디어 signature를 생성할 준비가 끝났다. HMAC은 Swift의 CryptoKit을 사용하면 쉽게 가능하다.
private var signatureKey: SymmetricKey {
guard let secretKey = Bundle.main.infoDictionary!["AWS_SECRET_KEY"] as? String else{
fatalError("SECRET KEY 없음")
}
let kDate = HMAC<SHA256>.authenticationCode(for: timestampString.data(using: .utf8)!, using: SymmetricKey(data:("AWS4" + secretKey).data(using: .utf8)!))
let kRegion = HMAC<SHA256>.authenticationCode(for: region.data(using: .utf8)!, using: SymmetricKey(data:kDate))
let kService = HMAC<SHA256>.authenticationCode(for: service.data(using: .utf8)!, using: SymmetricKey(data:kRegion))
return SymmetricKey(data:HMAC<SHA256>.authenticationCode(for: "aws4_request".data(using: .utf8)!, using: SymmetricKey(data:kService)))
}
let signature = HMAC<SHA256>.authenticationCode(for: stringToSign.data(using: .utf8)!, using: signatureKey)
.map{String(format: "%02hhx", $0)}.joined()
마지막으로 Authorization Header를 아래와 같이 만들고 요청을 보내면 성공! 만약 안된다면 response body를 출력해보면 안되는 이유를 알 수 있다.
let awsSignature = """
\(algorithm) Credential=\(accessKey)/\(credentialScope), \
SignedHeaders=\(signedHeaders), Signature=\(signature)
"""
private var headers: HTTPHeaders{
switch self {
case .saveFile:
return [
"Content-Type":"application/zip",
"Host":"\(host)",
"X-Amz-Content-SHA256":hashedPayload,
"X-Amz-Date":dateString,
"Authorization":awsSignature
]
}
}
참고
https://gist.github.com/elmyn/b63913d7ba4ffa26b37d55c7b7e260e1
https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
https://docs.aws.amazon.com/ko_kr/general/latest/gr/sigv4-signed-request-examples.html