180 lines
6.1 KiB
Swift
180 lines
6.1 KiB
Swift
#if canImport(CryptoKit)
|
|
import CryptoKit
|
|
#else
|
|
import Crypto
|
|
#endif
|
|
import Foundation
|
|
#if canImport(FoundationNetworking)
|
|
import FoundationNetworking
|
|
#endif
|
|
|
|
enum BedrockAWSSigner {
|
|
struct Credentials {
|
|
let accessKeyID: String
|
|
let secretAccessKey: String
|
|
let sessionToken: String?
|
|
}
|
|
|
|
static func sign(
|
|
request: inout URLRequest,
|
|
credentials: Credentials,
|
|
region: String,
|
|
service: String,
|
|
date: Date = Date())
|
|
{
|
|
let dateFormatter = Self.dateFormatter()
|
|
let dateStamp = Self.dateStamp(date: date)
|
|
let amzDate = dateFormatter.string(from: date)
|
|
|
|
request.setValue(amzDate, forHTTPHeaderField: "X-Amz-Date")
|
|
if let sessionToken = credentials.sessionToken {
|
|
request.setValue(sessionToken, forHTTPHeaderField: "X-Amz-Security-Token")
|
|
}
|
|
|
|
let host = request.url?.host ?? ""
|
|
request.setValue(host, forHTTPHeaderField: "Host")
|
|
|
|
let bodyHash = Self.sha256Hex(request.httpBody ?? Data())
|
|
request.setValue(bodyHash, forHTTPHeaderField: "x-amz-content-sha256")
|
|
|
|
let signedHeaders = Self.signedHeaders(request: request)
|
|
let canonicalRequest = Self.canonicalRequest(
|
|
request: request,
|
|
signedHeaders: signedHeaders,
|
|
bodyHash: bodyHash)
|
|
|
|
let credentialScope = "\(dateStamp)/\(region)/\(service)/aws4_request"
|
|
let stringToSign = [
|
|
"AWS4-HMAC-SHA256",
|
|
amzDate,
|
|
credentialScope,
|
|
Self.sha256Hex(Data(canonicalRequest.utf8)),
|
|
].joined(separator: "\n")
|
|
|
|
let signature = Self.calculateSignature(
|
|
secretKey: credentials.secretAccessKey,
|
|
dateStamp: dateStamp,
|
|
region: region,
|
|
service: service,
|
|
stringToSign: stringToSign)
|
|
|
|
let authorization = "AWS4-HMAC-SHA256 "
|
|
+ "Credential=\(credentials.accessKeyID)/\(credentialScope), "
|
|
+ "SignedHeaders=\(signedHeaders.keys), "
|
|
+ "Signature=\(signature)"
|
|
|
|
request.setValue(authorization, forHTTPHeaderField: "Authorization")
|
|
}
|
|
|
|
private struct SignedHeadersInfo {
|
|
let keys: String
|
|
let canonical: String
|
|
}
|
|
|
|
private static func signedHeaders(request: URLRequest) -> SignedHeadersInfo {
|
|
var headers: [(String, String)] = []
|
|
if let allHeaders = request.allHTTPHeaderFields {
|
|
for (key, value) in allHeaders {
|
|
headers.append((key.lowercased(), value.trimmingCharacters(in: .whitespaces)))
|
|
}
|
|
}
|
|
headers.sort { $0.0 < $1.0 }
|
|
|
|
let keys = headers.map(\.0).joined(separator: ";")
|
|
let canonical = headers.map { "\($0.0):\($0.1)" }.joined(separator: "\n")
|
|
return SignedHeadersInfo(keys: keys, canonical: canonical)
|
|
}
|
|
|
|
private static func canonicalRequest(
|
|
request: URLRequest,
|
|
signedHeaders: SignedHeadersInfo,
|
|
bodyHash: String) -> String
|
|
{
|
|
let method = request.httpMethod ?? "GET"
|
|
let url = request.url!
|
|
let path = url.path.isEmpty ? "/" : url.path
|
|
let query = Self.canonicalQueryString(url: url)
|
|
|
|
return [
|
|
method,
|
|
Self.uriEncodePath(path),
|
|
query,
|
|
signedHeaders.canonical + "\n",
|
|
signedHeaders.keys,
|
|
bodyHash,
|
|
].joined(separator: "\n")
|
|
}
|
|
|
|
private static func canonicalQueryString(url: URL) -> String {
|
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
|
let queryItems = components.queryItems,
|
|
!queryItems.isEmpty
|
|
else {
|
|
return ""
|
|
}
|
|
|
|
return queryItems
|
|
.map { item in
|
|
let key = Self.uriEncode(item.name)
|
|
let value = Self.uriEncode(item.value ?? "")
|
|
return "\(key)=\(value)"
|
|
}
|
|
.sorted()
|
|
.joined(separator: "&")
|
|
}
|
|
|
|
private static func calculateSignature(
|
|
secretKey: String,
|
|
dateStamp: String,
|
|
region: String,
|
|
service: String,
|
|
stringToSign: String) -> String
|
|
{
|
|
let kDate = Self.hmacSHA256(key: Data("AWS4\(secretKey)".utf8), data: Data(dateStamp.utf8))
|
|
let kRegion = Self.hmacSHA256(key: kDate, data: Data(region.utf8))
|
|
let kService = Self.hmacSHA256(key: kRegion, data: Data(service.utf8))
|
|
let kSigning = Self.hmacSHA256(key: kService, data: Data("aws4_request".utf8))
|
|
let signature = Self.hmacSHA256(key: kSigning, data: Data(stringToSign.utf8))
|
|
return signature.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
|
|
private static func hmacSHA256(key: Data, data: Data) -> Data {
|
|
let symmetricKey = SymmetricKey(data: key)
|
|
let mac = HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey)
|
|
return Data(mac)
|
|
}
|
|
|
|
private static func sha256Hex(_ data: Data) -> String {
|
|
let digest = SHA256.hash(data: data)
|
|
return digest.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
|
|
private static func dateFormatter() -> DateFormatter {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
|
formatter.timeZone = TimeZone(identifier: "UTC")
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
return formatter
|
|
}
|
|
|
|
private static func dateStamp(date: Date) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyyMMdd"
|
|
formatter.timeZone = TimeZone(identifier: "UTC")
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
private static func uriEncode(_ string: String) -> String {
|
|
var allowed = CharacterSet.alphanumerics
|
|
allowed.insert(charactersIn: "-._~")
|
|
return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? string
|
|
}
|
|
|
|
private static func uriEncodePath(_ path: String) -> String {
|
|
path.split(separator: "/", omittingEmptySubsequences: false)
|
|
.map { self.uriEncode(String($0)) }
|
|
.joined(separator: "/")
|
|
}
|
|
}
|