Files
2026-07-13 12:22:33 +08:00

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: "/")
}
}