Sources/OSS/Signer/SignerV1.swift (183 lines of code) (raw):

import Crypto import Foundation public class SignerV1: Signer { static let rfc822Datetime = createDateFormatter(dateFormat: "EE, dd MMM yyyy HH:mm:ss zzz") /// 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 init() {} 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 } private func authHeader(request: inout RequestMessage, signingContext: inout SigningContext) throws { // setp 1 preAuthHeader(request: &request, context: &signingContext) // setp 2 let sigingKey = try calcSigningKey(context: &signingContext) // setp 3 let signature = try 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 = try calcSigningKey(context: &signingContext) // setp 3 let signature = try calcSignature(signingKey: sigingKey, signToString: signingContext.stringToSign) // setp 4 postAuthQuery(request: &request, context: &signingContext, signature: signature) } /// add credential information and calc StringToSign for request func preAuthHeader(request: inout RequestMessage, context: inout SigningContext) { guard let credentials = context.credentials else { return } let dateTime = context.signTime ?? Date().addingTimeInterval(context.clockOffset ?? 0) let date = Self.rfc822Datetime.string(from: dateTime) // add date header request.headers["Date"] = date request.headers["x-oss-security-token"] = credentials.securityToken let stringToSign = buildStringToSign(request: request, resourcePath: resourcePath(bucket: context.bucket, key: context.key), date: date, headers: request.headers, subResource: context.subResource) context.stringToSign = stringToSign context.dateToSign = date } /// update authorization header func postAuthHeader(request: inout RequestMessage, context: inout SigningContext, signature: String) { guard let credentials = context.credentials else { return } request.headers["Authorization"] = "OSS \(credentials.accessKeyId):\(signature)" } /// add credential information and calc StringToSign for request func preAuthQuery(request: inout RequestMessage, context: inout SigningContext) { guard let credentials = context.credentials else { return } let expirationTime = context.expirationTime ?? Date().addingTimeInterval(context.clockOffset ?? 0).addingTimeInterval(15 * 60) let expiration = Int(expirationTime.timeIntervalSince1970) context.expirationTime = expirationTime var queryItems: [URLQueryItem] = [] if let securityToken = credentials.securityToken { queryItems.append(URLQueryItem(name: "security-token", value: securityToken.urlEncode())) } request.requestUri = request.requestUri.appending(queryItems) let stringToSign = buildStringToSign(request: request, resourcePath: resourcePath(bucket: context.bucket, key: context.key), date: "\(expiration)", headers: request.headers, subResource: context.subResource) context.stringToSign = stringToSign context.dateToSign = "\(expiration)" } func postAuthQuery(request: inout RequestMessage, context: inout SigningContext, signature: String) { guard let credentials = context.credentials else { return } var queryItems: [URLQueryItem] = [] queryItems.append(URLQueryItem(name: "OSSAccessKeyId", value: credentials.accessKeyId.urlEncode())) queryItems.append(URLQueryItem(name: "Signature", value: signature.urlEncode())) queryItems.append(URLQueryItem(name: "Expires", value: "\(context.dateToSign)")) request.requestUri = request.requestUri.appending(queryItems) } private func buildStringToSign(request: RequestMessage, resourcePath: String, date: String, headers: [String: String], subResource: [String]?) -> String { let method = request.method let contentMD5 = request.headers["Content-MD5"] ?? "" let contentType = request.headers["Content-Type"] ?? "" let canonicalizedOSSHeaders = canonicalizedOSSHeaders(headers) let queryItems = URLComponents(url: request.requestUri, resolvingAgainstBaseURL: false)?.queryItems let canonicalizedResource = canonicalizedResource(resourcePath: resourcePath, queryItems: queryItems, subResource: subResource) // format content let stringToSign = """ \(method)\n\ \(contentMD5)\n\ \(contentType)\n\ \(date)\n\ \(canonicalizedOSSHeaders)\ \(canonicalizedResource) """ // print("StringToSign: \(stringToSign)") return stringToSign } private func canonicalizedOSSHeaders(_ headers: [String: String]) -> String { var _headers: [String: String] = [:] for (key, value) in headers { _headers[key.lowercased()] = value } let canonicalizedOSSHeaders = _headers.sorted { $0.key < $1.key }.map { "\($0.key):\($0.value)" }.filter { $0.hasPrefix("x-oss-") }.joined(separator: "\n") return _headers.count > 0 ? canonicalizedOSSHeaders.appending("\n") : canonicalizedOSSHeaders } private func canonicalizedResource(resourcePath: String, queryItems: [URLQueryItem]?, subResource: [String]?) -> String { if let queryItems = queryItems { let querys = queryItems.filter { signerResourceFlag.contains($0.name) || signerResponseFlag.contains($0.name) || $0.name == signerProcessFlag || $0.name.hasPrefix(signerAccessFlag) || (subResource?.contains($0.name) ?? false) }.compactMap { if let name = $0.name.trim().urlEncode() { return URLQueryItem(name: name, value: $0.value) } return nil }.sorted { $0.name < $1.name }.map { if let value = $0.value?.trim().urlEncode(), !value.isEmpty { return "\($0.name)=\(value)" } else { return "\($0.name)" } } if querys.count > 0 { return resourcePath.appending("?\(querys.joined(separator: "&"))") } } return resourcePath } 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) throws -> SymmetricKey { let key = context.credentials?.accessKeySecret.data(using: .utf8)! return SymmetricKey(data: key!) } func calcSignature(signingKey: SymmetricKey, signToString: String) throws -> String { let content = signToString.data(using: .utf8)! return Data(HMAC<Insecure.SHA1>.authenticationCode(for: content, using: signingKey)).base64EncodedString() } } extension URL { var queryItems: [URLQueryItem]? { guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return nil } return components.queryItems } func appending(_ items: [URLQueryItem]) -> URL { guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return absoluteURL } var queryItems = components.queryItems ?? [] queryItems.append(contentsOf: items) components.queryItems = queryItems return components.url! } }