Sources/OSS/Signer/SignerV4.swift (226 lines of code) (raw):

import Crypto import Foundation public class SignerV4: Signer { static let rfc822Datetime = createDateFormatter(dateFormat: "EE, dd MMM yyyy HH:mm:ss zzz") static let iso8601Datetime = createDateFormatter(dateFormat: "yyyyMMdd'T'HHmmss'Z'") public init() {} /// create timestamp dateformatter static func createDateFormatter(dateFormat: String) -> DateFormatter { let formatter = DateFormatter() formatter.dateFormat = dateFormat formatter.timeZone = TimeZone(abbreviation: "UTC") formatter.locale = Locale(identifier: "en_US_POSIX") return formatter } public func sign(request: RequestMessage, signingContext: inout SigningContext) async throws -> RequestMessage { var request = request if signingContext.authHeader { try authHeader(request: &request, signingContext: &signingContext) } else { try authQuery(request: &request, signingContext: &signingContext) } return request } /// add credential information and calc StringToSign for request func preAuthHeader(request: inout RequestMessage, context: inout SigningContext) { let cred = context.credentials! let region = context.region ?? "" let product = context.product ?? "" // Date let signTime = context.signTime ?? Date().addingTimeInterval(context.clockOffset ?? 0) let datetime = Self.iso8601Datetime.string(from: signTime) let date = String(datetime[datetime.startIndex ..< datetime.index(datetime.startIndex, offsetBy: 8)]) let datetimeGmt = Self.rfc822Datetime.string(from: signTime) // Scope let scope = "\(date)/\(region)/\(product)/aliyun_v4_request" // Credential information signature if let securityToken = cred.securityToken, !securityToken.isEmpty { request.headers["x-oss-security-token"] = cred.securityToken } // Other Headers request.headers["x-oss-content-sha256"] = "UNSIGNED-PAYLOAD" request.headers["x-oss-date"] = datetime request.headers["Date"] = datetimeGmt // Lowercase request headers var headers: [String: String] = [:] for (key, value) in request.headers { headers[key.lowercased()] = value } // let headers = request.headers // Lowercase additional headers var additionalSignedHeaders: [String] = [] context.additionalHeaderNames?.forEach { key in let lowkey = key.lowercased() if !(lowkey == "content-md5" || lowkey == "content-type" || lowkey.hasPrefix("x-oss-")) && headers.keys.contains(lowkey) { additionalSignedHeaders.append(lowkey) } } // CanonicalRequest let canonicalRequest = calcCanonicalRequest( request: request, resourcePath: resourcePath(bucket: context.bucket, key: context.key), headers: headers, additionalHeaders: additionalSignedHeaders ) // StringToSign context.stringToSign = calcStringToSign(datetime: datetime, scope: scope, canonicalRequest: canonicalRequest) context.dateToSign = date context.scopeToSign = scope context.additionalHeadersToSign = additionalSignedHeaders.sorted().joined(separator: ";") } /// update authorization header func postAuthHeader(request: inout RequestMessage, context: inout SigningContext, signature: String) { let credential = "OSS4-HMAC-SHA256 Credential=\(context.credentials!.accessKeyId)/\(context.scopeToSign)" let signedHeaders = context.additionalHeadersToSign == "" ? "" : ",AdditionalHeaders=\(context.additionalHeadersToSign)" request.headers["Authorization"] = "\(credential)\(signedHeaders),Signature=\(signature)" } /// add credential information and calc StringToSign for request func preAuthQuery(request: inout RequestMessage, context: inout SigningContext) { let cred = context.credentials! let region = context.region ?? "" let product = context.product ?? "" // Date let signTime = context.signTime ?? Date().addingTimeInterval(context.clockOffset ?? 0) let datetime = Self.iso8601Datetime.string(from: signTime) let date = String(datetime[datetime.startIndex ..< datetime.index(datetime.startIndex, offsetBy: 8)]) // Expiration let expiration = context.expirationTime ?? signTime.addingTimeInterval(15 * 60) let expires = Int(expiration.timeIntervalSince1970 - signTime.timeIntervalSince1970) context.expirationTime = expiration // Scope let scope = "\(date)/\(region)/\(product)/aliyun_v4_request" // Other Headers // Lowercase request headers var headers: [String: String] = [:] for (key, value) in request.headers { headers[key.lowercased()] = value } // Lowercase additional headers var additionalSignedHeaders: [String] = [] context.additionalHeaderNames?.forEach { key in let lowkey = key.lowercased() if !(lowkey == "content-md5" || lowkey == "content-type" || lowkey.hasPrefix("x-oss-")) && headers.keys.contains(lowkey) { additionalSignedHeaders.append(lowkey) } } // Credential information signature var query = request.requestUri.query ?? "" if query.count > 0 { query += "&" } query += "x-oss-signature-version=OSS4-HMAC-SHA256" query += "&x-oss-date=\(datetime)" query += "&x-oss-expires=\(expires)" let credentialQuery = "\(cred.accessKeyId)/\(scope)" query += "&x-oss-credential=\(credentialQuery.urlEncode()!)" if additionalSignedHeaders.count > 0 { let addHeaderStr = additionalSignedHeaders.sorted().joined(separator: ";") query += "&x-oss-additional-headers=\(addHeaderStr.urlEncode()!)" } if let securityToken = cred.securityToken, !securityToken.isEmpty { query += "&x-oss-security-token=\(securityToken.urlEncode()!)" } request.requestUri = URL(string: request.requestUri.absoluteString.split(separator: "?")[0] + "?" + query)! // CanonicalRequest let canonicalRequest = calcCanonicalRequest( request: request, resourcePath: resourcePath(bucket: context.bucket, key: context.key), headers: headers, additionalHeaders: additionalSignedHeaders ) // StringToSign context.stringToSign = calcStringToSign(datetime: datetime, scope: scope, canonicalRequest: canonicalRequest) context.dateToSign = date context.scopeToSign = scope context.additionalHeadersToSign = additionalSignedHeaders.sorted().joined(separator: ";") } func postAuthQuery(request: inout RequestMessage, context _: inout SigningContext, signature: String) { // Credential var query = request.requestUri.query ?? "" query += "&x-oss-signature=\(signature)" request.requestUri = URL(string: request.requestUri.absoluteString.split(separator: "?")[0] + "?" + query)! } func calcCanonicalRequest(request: RequestMessage, resourcePath: String, headers: [String: String], additionalHeaders: [String]) -> String { /* Canonical Request HTTP Verb + "\n" + Canonical URI + "\n" + Canonical Query String + "\n" + Canonical Headers + "\n" + Additional Headers + "\n" + Hashed PayLoad */ let verb = request.method.uppercased() let canonicalUri = resourcePath.urlEncodeWithoutSeparator()! let canonicalQueries: String let urlComps = URLComponents(url: request.requestUri, resolvingAgainstBaseURL: false)! let queryItems = urlComps.queryItems // print("queryItems: \(queryItems)\n") if queryItems != nil { canonicalQueries = queryItems! .map { (name: $0.name.urlEncode()!, value: $0.value?.urlEncode()) } .sorted { $0.name < $1.name } .map { if let value = $0.value, value != "" { return "\($0.name)=\(value)" } return "\($0.name)" } .joined(separator: "&") } else { canonicalQueries = "" } let canonicalHeaders = headers .filter { if $0.key == "content-md5" || $0.key == "content-type" || $0.key.hasPrefix("x-oss-") || additionalHeaders.contains($0.key) { return true } return false } .map { (key: $0.key, value: $0.value.trim()) } .sorted { $0.key < $1.key } .map { "\($0.key):\($0.value)\n" } .joined(separator: "") let canonicalAdditionalHeaders = additionalHeaders .sorted() .joined(separator: ";") let hashedPayLoad = headers["x-oss-content-sha256"] ?? "UNSIGNED-PAYLOAD" let canonicalRequest = """ \(verb)\n\ \(canonicalUri)\n\ \(canonicalQueries)\n\ \(canonicalHeaders)\n\ \(canonicalAdditionalHeaders)\n\ \(hashedPayLoad) """ // print("canonicalRequest:\n\(canonicalRequest)\n") return canonicalRequest } func calcStringToSign(datetime: String, scope: String, canonicalRequest: String) -> String { /* StringToSign "OSS4-HMAC-SHA256" + "\n" + TimeStamp + "\n" + Scope + "\n" + Hex(SHA256Hash(Canonical Request)) */ let h = SHA256.hash(data: canonicalRequest.data(using: .utf8)!) let hexDigest = h.compactMap { String(format: "%02x", $0) }.joined() let stringToSign = """ OSS4-HMAC-SHA256\n\ \(datetime)\n\ \(scope)\n\ \(hexDigest) """ // print("stringToSign:\n\(stringToSign)\n") return stringToSign } func resourcePath(bucket: String?, key: String?) -> String { var resourcePath = "/" + (bucket ?? "") + (key != nil ? "/" + key! : "") if bucket != nil && key == nil { resourcePath = resourcePath + "/" } return resourcePath } func calcSigningKey(context: inout SigningContext) -> SymmetricKey { let region = context.region ?? "" let product = context.product ?? "" let kDate = HMAC<SHA256>.authenticationCode( for: context.dateToSign.data(using: .utf8)!, using: SymmetricKey(data: Array("aliyun_v4\(context.credentials!.accessKeySecret)".utf8)) ) let kRegion = HMAC<SHA256>.authenticationCode(for: region.data(using: .utf8)!, using: SymmetricKey(data: kDate)) let kProduct = HMAC<SHA256>.authenticationCode(for: product.data(using: .utf8)!, using: SymmetricKey(data: kRegion)) let kSigning = HMAC<SHA256>.authenticationCode(for: [UInt8]("aliyun_v4_request".utf8), using: SymmetricKey(data: kProduct)) return SymmetricKey(data: kSigning) } func calcSignature(signingKey: SymmetricKey, signToString: String) -> String { let kSignature = HMAC<SHA256>.authenticationCode( for: signToString.data(using: .utf8)!, using: signingKey ) return kSignature.compactMap { String(format: "%02x", $0) }.joined() } private func authHeader(request: inout RequestMessage, signingContext: inout SigningContext) throws { // setp 1 preAuthHeader(request: &request, context: &signingContext) // setp 2 let sigingKey = calcSigningKey(context: &signingContext) // setp 3 let signature = calcSignature(signingKey: sigingKey, signToString: signingContext.stringToSign) // setp 4 postAuthHeader(request: &request, context: &signingContext, signature: signature) } private func authQuery(request: inout RequestMessage, signingContext: inout SigningContext) throws { // setp 1 preAuthQuery(request: &request, context: &signingContext) // setp 2 let sigingKey = calcSigningKey(context: &signingContext) // setp 3 let signature = calcSignature(signingKey: sigingKey, signToString: signingContext.stringToSign) // setp 4 postAuthQuery(request: &request, context: &signingContext, signature: signature) } }