diff --git a/CHANGELOG.md b/CHANGELOG.md index d171f757..2481f9e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes +## Unreleased + +* Changes the `retry` flag to be a boolean instead of a string for prepUpload requests. This is a breaking change for stored offline jobs, where the job is written using an older sdk version and then submission is attempted using this version + ## 10.5.1 ### Fixed diff --git a/Example/Podfile.lock b/Example/Podfile.lock index c57e9feb..37cd440b 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -15,7 +15,9 @@ PODS: - SmileID (10.5.1): - FingerprintJS - lottie-ios (~> 4.5.0) + - SmileIDSecurity (~> 1.0.1) - ZIPFoundation (~> 0.9) + - SmileIDSecurity (1.0.1) - SwiftLint (0.58.2) - ZIPFoundation (0.9.19) @@ -33,6 +35,7 @@ SPEC REPOS: - lottie-ios - netfox - Sentry + - SmileIDSecurity - SwiftLint - ZIPFoundation @@ -51,7 +54,8 @@ SPEC CHECKSUMS: lottie-ios: 248b380fa1b97d18e792c37d90da7ab2aa0d6562 netfox: 9d5cc727fe7576c4c7688a2504618a156b7d44b7 Sentry: 1ca8405451040482877dcd344dfa3ef80b646631 - SmileID: 7f9c9db916d4c3997fcafb8b811afec30e88c751 + SmileID: 8c9228f3dcdad070af6cd3e6eae6a5aae1e8ccb2 + SmileIDSecurity: b847101f7d7b86c1c453fb1b045146dfcbc5183d SwiftLint: 365bcd9ffc83d0deb874e833556d82549919d6cd ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c diff --git a/Package.swift b/Package.swift index bc257966..b5278881 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,8 @@ let package = Package( dependencies: [ .package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0")), .package(url: "https://github.com/airbnb/lottie-spm", from: "4.5.0"), - .package(url: "https://github.com/fingerprintjs/fingerprintjs-ios", from: "1.5.0") + .package(url: "https://github.com/fingerprintjs/fingerprintjs-ios", from: "1.5.0"), + .package(url: "https://github.com/smileidentity/smile-id-security", from: "1.0.1") ], targets: [ .target( @@ -23,7 +24,8 @@ let package = Package( dependencies: [ .product(name: "ZIPFoundation", package: "ZIPFoundation"), .product(name: "FingerprintJS", package: "fingerprintjs-ios"), - .product(name: "Lottie", package: "lottie-spm") + .product(name: "Lottie", package: "lottie-spm"), + .product(name: "SmileIDSecurity", package: "smile-id-security") ], path: "Sources/SmileID", resources: [.process("Resources")] diff --git a/SmileID.podspec b/SmileID.podspec index aa21f9d1..d640d83c 100644 --- a/SmileID.podspec +++ b/SmileID.podspec @@ -10,6 +10,7 @@ Pod::Spec.new do |s| s.dependency 'ZIPFoundation', '~> 0.9' s.dependency 'FingerprintJS' s.dependency 'lottie-ios', '~> 4.5.0' + s.dependency 'SmileIDSecurity', '~> 1.0.1' s.swift_version = '5.5' s.source_files = 'Sources/SmileID/Classes/**/*' s.resource_bundles = { diff --git a/SmileID.xcodeproj/project.pbxproj b/SmileID.xcodeproj/project.pbxproj index f95431c3..a0143ab0 100644 --- a/SmileID.xcodeproj/project.pbxproj +++ b/SmileID.xcodeproj/project.pbxproj @@ -4000,6 +4000,7 @@ 1E4A02382BF4EB9D00167633 /* Lottie in Frameworks */, 6277E7512C65153700AC87FB /* ZIPFoundation in Frameworks */, 1E9825A32C9B4AF2009F2CA6 /* FingerprintJS in Frameworks */, + 6672CF0B2D88877E005DF7A6 /* SmileIDSecurity in Frameworks */, 1E9825A52C9B4AF2009F2CA6 /* SystemControl in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7990,6 +7991,7 @@ 6277E7502C65153700AC87FB /* ZIPFoundation */, 1E9825A22C9B4AF2009F2CA6 /* FingerprintJS */, 1E9825A42C9B4AF2009F2CA6 /* SystemControl */, + 6672CF0A2D88877E005DF7A6 /* SmileIDSecurity */, ); productName = SmileID; productReference = 1EEFC2382B583F1A00B8A934 /* libSmileID.a */; @@ -8032,6 +8034,7 @@ 1E6857C92BF39CDD0019B515 /* XCRemoteSwiftPackageReference "lottie-spm" */, 6277E74D2C65119600AC87FB /* XCRemoteSwiftPackageReference "ZIPFoundation" */, 1E98259D2C9B478C009F2CA6 /* XCRemoteSwiftPackageReference "fingerprintjs-ios" */, + 6672CF092D88877E005DF7A6 /* XCRemoteSwiftPackageReference "smile-id-security" */, ); productRefGroup = 1EEFC21B2B583CFB00B8A934 /* Products */; projectDirPath = ""; @@ -8747,6 +8750,14 @@ minimumVersion = 0.9.19; }; }; + 6672CF092D88877E005DF7A6 /* XCRemoteSwiftPackageReference "smile-id-security" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/smileidentity/smile-id-security"; + requirement = { + branch = main; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -8790,6 +8801,11 @@ package = 6277E74D2C65119600AC87FB /* XCRemoteSwiftPackageReference "ZIPFoundation" */; productName = ZIPFoundation; }; + 6672CF0A2D88877E005DF7A6 /* SmileIDSecurity */ = { + isa = XCSwiftPackageProductDependency; + package = 6672CF092D88877E005DF7A6 /* XCRemoteSwiftPackageReference "smile-id-security" */; + productName = SmileIDSecurity; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 1EEFC2112B583CFB00B8A934 /* Project object */; diff --git a/SmileID.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SmileID.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 84e075ce..d0f4175d 100644 --- a/SmileID.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SmileID.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b4a832f8609e6d8fe26567f801eacf41b54111a27551c5d2a949decba83d8b82", + "originHash" : "60ccf1f3bdb356f89e96f70a8531c34eebffe2363a31000c069923d65f56f54d", "pins" : [ { "identity" : "fingerprintjs-ios", @@ -19,6 +19,15 @@ "version" : "4.4.3" } }, + { + "identity" : "smile-id-security", + "kind" : "remoteSourceControl", + "location" : "https://github.com/smileidentity/smile-id-security", + "state" : { + "branch" : "main", + "revision" : "77c8d918c2e21e88a6f87504785429d8015aa0fc" + } + }, { "identity" : "zipfoundation", "kind" : "remoteSourceControl", diff --git a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift index 166baa0c..297ea74a 100644 --- a/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift +++ b/Sources/SmileID/Classes/BiometricKYC/OrchestratedBiometricKycViewModel.swift @@ -63,8 +63,7 @@ class OrchestratedBiometricKycViewModel: ObservableObject { func onFinished(delegate: BiometricKycResultDelegate) { if let selfieFile = selfieFile, let livenessFiles = livenessFiles, - let selfiePath = getRelativePath(from: selfieFile) - { + let selfiePath = getRelativePath(from: selfieFile) { delegate.didSucceed( selfieImage: selfiePath, livenessImages: livenessFiles.compactMap { getRelativePath(from: $0) }, @@ -143,6 +142,11 @@ class OrchestratedBiometricKycViewModel: ObservableObject { if let livenessFiles { allFiles.append(contentsOf: livenessFiles) } + do { + if let securityInfoJson = try LocalStorage.addSecurityInfo(jobId: jobId, files: allFiles) { + allFiles.append(contentsOf: [securityInfoJson]) + } + } catch { /* in case we can't add the security info the backend will deal with the enrollment */ } return try LocalStorage.zipFiles(at: allFiles) } @@ -176,7 +180,7 @@ class OrchestratedBiometricKycViewModel: ObservableObject { } private func prepareForUpload(authResponse: AuthenticationResponse) async throws -> PrepUploadResponse { - let prepUploadRequest = PrepUploadRequest( + var prepUploadRequest = PrepUploadRequest( partnerParams: authResponse.partnerParams.copy(extras: extraPartnerParams), allowNewEnroll: allowNewEnroll, metadata: localMetadata.metadata.items, @@ -193,8 +197,10 @@ class OrchestratedBiometricKycViewModel: ObservableObject { else { throw error } + prepUploadRequest.retry = true return try await SmileID.api.prepUpload( - request: prepUploadRequest.copy(retry: "true")) + request: prepUploadRequest + ) } } diff --git a/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift b/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift index 6bf4d18b..1855f54a 100644 --- a/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift +++ b/Sources/SmileID/Classes/DocumentVerification/Model/OrchestratedDocumentVerificationViewModel.swift @@ -179,6 +179,11 @@ class IOrchestratedDocumentVerificationViewModel: ObservableObj livenessImages: livenessFiles ) allFiles.append(info) + do { + if let securityInfoJson = try LocalStorage.addSecurityInfo(jobId: jobId, files: allFiles) { + allFiles.append(contentsOf: [securityInfoJson]) + } + } catch { /* in case we can't add the security info the backend will deal with the enrollment */ } let zipData = try LocalStorage.zipFiles(at: allFiles) self.savedFiles = DocumentCaptureResultStore( allFiles: allFiles, @@ -209,7 +214,7 @@ class IOrchestratedDocumentVerificationViewModel: ObservableObj ) } let authResponse = try await SmileID.api.authenticate(request: authRequest) - let prepUploadRequest = PrepUploadRequest( + var prepUploadRequest = PrepUploadRequest( partnerParams: authResponse.partnerParams.copy(extras: self.extraPartnerParams), allowNewEnroll: allowNewEnroll, metadata: localMetadata.metadata.items, @@ -224,8 +229,9 @@ class IOrchestratedDocumentVerificationViewModel: ObservableObj } catch let error as SmileIDError { switch error { case .api("2215", _): + prepUploadRequest.retry = true prepUploadResponse = try await SmileID.api.prepUpload( - request: prepUploadRequest.copy(retry: "true") + request: prepUploadRequest ) default: throw error @@ -346,9 +352,7 @@ extension IOrchestratedDocumentVerificationViewModel: SmartSelfieResultDelegate } // swiftlint:disable opening_brace -class OrchestratedDocumentVerificationViewModel: - IOrchestratedDocumentVerificationViewModel -{ +class OrchestratedDocumentVerificationViewModel: IOrchestratedDocumentVerificationViewModel { override func onFinished(delegate: DocumentVerificationResultDelegate) { if let savedFiles, let selfiePath = getRelativePath(from: selfieFile), @@ -372,11 +376,7 @@ class OrchestratedDocumentVerificationViewModel: } // swiftlint:disable opening_brace -class OrchestratedEnhancedDocumentVerificationViewModel: - IOrchestratedDocumentVerificationViewModel< - EnhancedDocumentVerificationResultDelegate, EnhancedDocumentVerificationJobResult - > -{ +class OrchestratedEnhancedDocumentVerificationViewModel: IOrchestratedDocumentVerificationViewModel { override func onFinished(delegate: EnhancedDocumentVerificationResultDelegate) { if let savedFiles, let selfiePath = getRelativePath(from: selfieFile), diff --git a/Sources/SmileID/Classes/Helpers/LocalStorage.swift b/Sources/SmileID/Classes/Helpers/LocalStorage.swift index 332172e9..129d9642 100644 --- a/Sources/SmileID/Classes/Helpers/LocalStorage.swift +++ b/Sources/SmileID/Classes/Helpers/LocalStorage.swift @@ -1,5 +1,6 @@ import Foundation import ZIPFoundation +import SmileIDSecurity public class LocalStorage { private static let defaultFolderName = "SmileID" @@ -133,6 +134,39 @@ public class LocalStorage { return try createSmileFile(to: jobId, name: "info.json", file: data) } + static func addSecurityInfo( + jobId: String, + files: [URL] + ) throws -> URL? { + do { + let timestamp = Date().toISO8601WithMilliseconds() + let mac = try SmileIDCryptoManager.shared.sign( + timestamp: timestamp, + files: files + ) + let securityInfo = SecurityInfo( + timestamp: timestamp, + mac: mac + ) + let securityInfoJson = try createSecurityInfoFile( + jobId: jobId, + securityInfo: securityInfo + ) + return securityInfoJson + } catch { + print("Couldn't create security info. Continuing without it.") + return nil + } + } + + private static func createSecurityInfoFile( + jobId: String, + securityInfo: SecurityInfo + ) throws -> URL { + let data = try jsonEncoder.encode(securityInfo) + return try createSmileFile(to: jobId, name: "security_info.json", file: data) + } + static func getInfoJsonFile( jobId: String ) throws -> URL { diff --git a/Sources/SmileID/Classes/Networking/Models/FailureReason.swift b/Sources/SmileID/Classes/Networking/Models/FailureReason.swift index f68bcd02..0dd749d6 100644 --- a/Sources/SmileID/Classes/Networking/Models/FailureReason.swift +++ b/Sources/SmileID/Classes/Networking/Models/FailureReason.swift @@ -1,6 +1,6 @@ import Foundation -public enum FailureReason: Encodable { +public enum FailureReason: Codable { case mobileActiveLivenessTimeout private enum CodingKeys: String, CodingKey { diff --git a/Sources/SmileID/Classes/Networking/Models/MultipartBody.swift b/Sources/SmileID/Classes/Networking/Models/MultipartBody.swift index 3118f230..8b122e84 100644 --- a/Sources/SmileID/Classes/Networking/Models/MultipartBody.swift +++ b/Sources/SmileID/Classes/Networking/Models/MultipartBody.swift @@ -1,15 +1,22 @@ import Foundation -public struct MultipartBody: Encodable { - let key: String - let filename: String +public struct MultipartBody: Codable { let data: Data + let filename: String let mimeType: String - public init?(withImage image: Data, forKey key: String, forName name: String) { - self.key = key - mimeType = "image/jpeg" - filename = name - data = image + public init?( + withImage image: Data, + forName name: String + ) { + self.data = image + self.filename = name + self.mimeType = "image/jpeg" + } + + enum CodingKeys: String, CodingKey { + case data + case filename = "name" + case mimeType = "type" } } diff --git a/Sources/SmileID/Classes/Networking/Models/PrepUpload.swift b/Sources/SmileID/Classes/Networking/Models/PrepUpload.swift index 1b8119b0..36195a5a 100644 --- a/Sources/SmileID/Classes/Networking/Models/PrepUpload.swift +++ b/Sources/SmileID/Classes/Networking/Models/PrepUpload.swift @@ -12,7 +12,7 @@ public struct PrepUploadRequest: Codable { public var timestamp = Date().toISO8601WithMilliseconds() public var signature = "" public var useEnrolledImage = false - public var retry = "false" /// backend is broken needs these as strings + public var retry: Bool = false public init( partnerParams: PartnerParams, @@ -25,7 +25,7 @@ public struct PrepUploadRequest: Codable { timestamp: String = Date().toISO8601WithMilliseconds(), signature: String = "", useEnrolledImage: Bool = false, - retry: String = "false" + retry: Bool = false ) { self.partnerParams = partnerParams self.callbackUrl = callbackUrl @@ -53,22 +53,6 @@ public struct PrepUploadRequest: Codable { case retry case metadata } - - public func copy(retry: String? = nil) -> PrepUploadRequest { - return PrepUploadRequest( - partnerParams: partnerParams, - callbackUrl: callbackUrl, - allowNewEnroll: allowNewEnroll, - partnerId: partnerId, - metadata: metadata, - sourceSdk: sourceSdk, - sourceSdkVersion: sourceSdkVersion, - timestamp: timestamp, - signature: signature, - useEnrolledImage: useEnrolledImage, - retry: retry ?? self.retry - ) - } } public struct PrepUploadResponse: Codable { diff --git a/Sources/SmileID/Classes/Networking/Models/SecurityInfo.swift b/Sources/SmileID/Classes/Networking/Models/SecurityInfo.swift new file mode 100644 index 00000000..48f1abe6 --- /dev/null +++ b/Sources/SmileID/Classes/Networking/Models/SecurityInfo.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct SecurityInfo: Codable { + public var timestamp: String + public var mac: String + + public init( + timestamp: String, + mac: String + ) { + self.timestamp = timestamp + self.mac = mac + } + + enum CodingKeys: String, CodingKey { + case timestamp + case mac + } +} diff --git a/Sources/SmileID/Classes/Networking/Models/Selfie.swift b/Sources/SmileID/Classes/Networking/Models/Selfie.swift new file mode 100644 index 00000000..e33b9c5f --- /dev/null +++ b/Sources/SmileID/Classes/Networking/Models/Selfie.swift @@ -0,0 +1,70 @@ +import Foundation + +public struct SelfieRequest: Codable { + public var selfieImage: Data + public var livenessImages: [Data] + public var userId: String? + public var partnerParams: [String: String]? + public var callbackUrl: String? + public var sandboxResult: Int? + public var allowNewEnroll: Bool? + public var failureReason: FailureReason? + public var metadata: [Metadatum] + + public init( + selfieImage: Data, + livenessImages: [Data], + userId: String? = nil, + partnerParams: [String: String]? = nil, + callbackUrl: String? = nil, + sandboxResult: Int? = nil, + allowNewEnroll: Bool? = nil, + failureReason: FailureReason? = nil, + metadata: [Metadatum] + ) { + self.selfieImage = selfieImage + self.livenessImages = livenessImages + self.userId = userId + self.partnerParams = partnerParams + self.callbackUrl = callbackUrl + self.sandboxResult = sandboxResult + self.allowNewEnroll = allowNewEnroll + self.failureReason = failureReason + self.metadata = metadata + } + + enum CodingKeys: String, CodingKey { + case selfieImage = "selfie_image" + case livenessImages = "liveness_images" + case userId = "user_id" + case partnerParams = "partner_params" + case callbackUrl = "callback_url" + case sandboxResult = "sandbox_result" + case allowNewEnroll = "allow_new_enroll" + case failureReason = "failure_reason" + case metadata + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(selfieImage.base64EncodedString(), forKey: .selfieImage) + try container.encode(livenessImages.map { $0.base64EncodedString() }, forKey: .livenessImages) + if let userId = userId { + try container.encode(userId, forKey: .userId) + } + try container.encode(partnerParams, forKey: .partnerParams) + if let callbackUrl = callbackUrl { + try container.encode(callbackUrl, forKey: .callbackUrl) + } + if let sandboxResult = sandboxResult { + try container.encode(sandboxResult, forKey: .sandboxResult) + } + if let allowNewEnroll = allowNewEnroll { + try container.encode(allowNewEnroll, forKey: .allowNewEnroll) + } + if let failureReason = failureReason { + try container.encode(failureReason, forKey: .failureReason) + } + try container.encode(metadata, forKey: .metadata) + } +} diff --git a/Sources/SmileID/Classes/Networking/ServiceHeaderProvider.swift b/Sources/SmileID/Classes/Networking/ServiceHeaderProvider.swift index 23f26974..28a1ea03 100644 --- a/Sources/SmileID/Classes/Networking/ServiceHeaderProvider.swift +++ b/Sources/SmileID/Classes/Networking/ServiceHeaderProvider.swift @@ -41,4 +41,12 @@ public extension HTTPHeader { static func sourceSDKVersion(value: String) -> HTTPHeader { HTTPHeader(name: "SmileID-Source-SDK-Version", value: value) } + + static func requestTimestamp(value: String) -> HTTPHeader { + return HTTPHeader(name: "SmileID-Request-Timestamp", value: value) + } + + static func requestMac(value: String) -> HTTPHeader { + HTTPHeader(name: "SmileID-Request-Mac", value: value) + } } diff --git a/Sources/SmileID/Classes/Networking/ServiceRunnable.swift b/Sources/SmileID/Classes/Networking/ServiceRunnable.swift index f8d7265b..842ba899 100644 --- a/Sources/SmileID/Classes/Networking/ServiceRunnable.swift +++ b/Sources/SmileID/Classes/Networking/ServiceRunnable.swift @@ -1,4 +1,6 @@ import Foundation +import SmileIDSecurity +import UIKit // todo remove after testing protocol ServiceRunnable { var serviceClient: RestServiceClient { get } @@ -66,7 +68,8 @@ extension ServiceRunnable { .contentType(value: "application/json"), .partnerID(value: SmileID.config.partnerId), .sourceSDK(value: "iOS"), - .sourceSDKVersion(value: SmileID.version) + .sourceSDKVersion(value: SmileID.version), + .requestTimestamp(value: Date().toISO8601WithMilliseconds()) ], body: body ) @@ -80,7 +83,8 @@ extension ServiceRunnable { headers: [ .partnerID(value: SmileID.config.partnerId), .sourceSDK(value: "iOS"), - .sourceSDKVersion(value: SmileID.version) + .sourceSDKVersion(value: SmileID.version), + .requestTimestamp(value: Date().toISO8601WithMilliseconds()) ] ) return try await serviceClient.send(request: request) @@ -108,6 +112,45 @@ extension ServiceRunnable { headers.append(.timestamp(value: timestamp)) headers.append(.sourceSDK(value: "iOS")) headers.append(.sourceSDKVersion(value: SmileID.version)) + let timestamp = Date().toISO8601WithMilliseconds() + headers.append(.requestTimestamp(value: timestamp)) + + let startDate = Date() + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let selfieRequest = SelfieRequest( + selfieImage: selfieImage.data, + livenessImages: livenessImages.map { $0.data }, + userId: userId, + partnerParams: partnerParams, + callbackUrl: callbackUrl?.nilIfEmpty(), + sandboxResult: sandboxResult, + allowNewEnroll: allowNewEnroll, + failureReason: failureReason, + metadata: metadata.items + ) + do { + let payload = try encoder.encode(selfieRequest) + let requestMac = try SmileIDCryptoManager.shared.sign( + timestamp: timestamp, + headers: headers.toDictionary(), + payload: payload + ) + headers.append(.requestMac(value: requestMac)) + } catch { /* in case we can't add the security info the backend will deal with the enrollment */ } + let endDate = Date() + let time = endDate.timeIntervalSince(startDate) + print("Time for payload signing: \(time)") + + DispatchQueue.main.async { + let vc = UIApplication.getTopViewController() + let alert = UIAlertController(title: "Payload Signing Timing", message: String(time), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + if let vc = vc { + vc.present(alert, animated: true, completion: nil) + } + } + let request = try await createMultiPartRequest( url: path, method: .post, @@ -132,7 +175,7 @@ extension ServiceRunnable { private func createMultiPartRequest( url: PathType, method: RestMethod, - headers: [HTTPHeader]? = nil, + headers: [HTTPHeader], uploadData: Data ) async throws -> RestRequest { let path = String(describing: url) @@ -174,13 +217,14 @@ extension ServiceRunnable { private func createUploadRequest( url: String, method: RestMethod, - headers: [HTTPHeader]? = nil, + headers: [HTTPHeader], uploadData: Data, queryParameters _: [HTTPQueryParameters]? = nil ) async throws -> RestRequest { guard let url = URL(string: url) else { throw URLError(.badURL) } + let request = RestRequest( url: url, method: method, @@ -193,7 +237,7 @@ extension ServiceRunnable { private func createRestRequest( path: PathType, method: RestMethod, - headers: [HTTPHeader]? = nil, + headers: [HTTPHeader], queryParameters: [HTTPQueryParameters]? = nil, body: T ) async throws -> RestRequest { @@ -202,11 +246,26 @@ extension ServiceRunnable { throw URLError(.badURL) } + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + var signedHeaders = headers + do { + let payload = try encoder.encode(body) + if let header = headers.first(where: { $0.name == "SmileID-Request-Timestamp" }) { + let requestMac = try SmileIDCryptoManager.shared.sign( + timestamp: header.value, + headers: headers.toDictionary(), + payload: payload + ) + signedHeaders += [.requestMac(value: requestMac)] + } + } catch { /* in case we can't add the security info the backend will deal with the enrollment */ } + do { let request = try RestRequest( url: url, method: method, - headers: headers, + headers: signedHeaders, queryParameters: queryParameters, body: body ) @@ -219,7 +278,7 @@ extension ServiceRunnable { private func createRestRequest( path: PathType, method: RestMethod, - headers: [HTTPHeader]? = nil, + headers: [HTTPHeader], queryParameters: [HTTPQueryParameters]? = nil ) throws -> RestRequest { let path = String(describing: path) @@ -227,10 +286,21 @@ extension ServiceRunnable { throw URLError(.badURL) } + var signedHeaders = headers + do { + if let header = headers.first(where: { $0.name == "SmileID-Request-Timestamp" }) { + let requestMac = try SmileIDCryptoManager.shared.sign( + timestamp: header.value, + headers: headers.toDictionary() + ) + signedHeaders += [.requestMac(value: requestMac)] + } + } catch { /* in case we can't add the security info the backend will deal with the enrollment */ } + let request = RestRequest( url: url, method: method, - headers: headers, + headers: signedHeaders, queryParameters: queryParameters ) return request @@ -267,7 +337,10 @@ extension ServiceRunnable { body.append(dispositionData) body.append(contentTypeData) - if let jsonData = try? JSONSerialization.data(withJSONObject: parameters, options: []) { + if let jsonData = try? JSONSerialization.data( + withJSONObject: parameters, + options: [.sortedKeys, .withoutEscapingSlashes] + ) { body.append(jsonData) body.append(lineBreakData) } @@ -319,6 +392,7 @@ extension ServiceRunnable { // Append metadata let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] if let metadataData = try? encoder.encode(metadata.items) { body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!) body.append("Content-Disposition: form-data; name=\"metadata\"\(lineBreak)".data(using: .utf8)!) @@ -362,3 +436,32 @@ extension ServiceRunnable { return body } } + +extension UIApplication { + class func getTopViewController( + base: UIViewController? = UIWindow.current?.rootViewController + ) -> UIViewController? { + if let navController = base as? UINavigationController { + return getTopViewController(base: navController.visibleViewController) + } else if let tabController = base as? UITabBarController, + let selected = tabController.selectedViewController + { + return getTopViewController(base: selected) + } else if let presented = base?.presentedViewController { + return getTopViewController(base: presented) + } + return base + } +} + +extension UIWindow { + static var current: UIWindow? { + for scene in UIApplication.shared.connectedScenes { + guard let windowScene = scene as? UIWindowScene else { continue } + for window in windowScene.windows { + if window.isKeyWindow { return window } + } + } + return nil + } +} diff --git a/Sources/SmileID/Classes/SelfieCapture/SelfieSubmissionManager.swift b/Sources/SmileID/Classes/SelfieCapture/SelfieSubmissionManager.swift index 62173efb..b2ea26b9 100644 --- a/Sources/SmileID/Classes/SelfieCapture/SelfieSubmissionManager.swift +++ b/Sources/SmileID/Classes/SelfieCapture/SelfieSubmissionManager.swift @@ -118,7 +118,6 @@ final class SelfieSubmissionManager { } return MultipartBody( withImage: imageData, - forKey: fileURL.lastPathComponent, forName: fileURL.lastPathComponent ) } diff --git a/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift b/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift index 8e648125..629e39ea 100644 --- a/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift +++ b/Sources/SmileID/Classes/SelfieCapture/SelfieViewModel.swift @@ -422,7 +422,6 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { if let selfie = try? Data(contentsOf: selfieImage), let media = MultipartBody( withImage: selfie, - forKey: selfieImage.lastPathComponent, forName: selfieImage.lastPathComponent ) { @@ -433,7 +432,6 @@ public class SelfieViewModel: ObservableObject, ARKitSmileDelegate { if let data = try? Data(contentsOf: liveness) { return MultipartBody( withImage: data, - forKey: liveness.lastPathComponent, forName: liveness.lastPathComponent ) } diff --git a/Sources/SmileID/Classes/SmileID.swift b/Sources/SmileID/Classes/SmileID.swift index 7823d221..8f65553b 100644 --- a/Sources/SmileID/Classes/SmileID.swift +++ b/Sources/SmileID/Classes/SmileID.swift @@ -6,7 +6,7 @@ import UIKit public class SmileID { /// The default value for `timeoutIntervalForRequest` for URLSession default configuration. public static let defaultRequestTimeout: TimeInterval = 60 - public static let version = "10.5.1" + public static let version = "10.99.99" @Injected var injectedApi: SmileIDServiceable public static var configuration: Config { config } @@ -182,7 +182,7 @@ public class SmileID { userId: authRequestFile.userId ) let authResponse = try await SmileID.api.authenticate(request: authRequest) - let prepUploadRequest = PrepUploadRequest( + var prepUploadRequest = PrepUploadRequest( partnerParams: authResponse.partnerParams.copy( extras: prepUploadFile.partnerParams.extras ), @@ -198,8 +198,9 @@ public class SmileID { } catch let error as SmileIDError { switch error { case .api("2215", _): + prepUploadRequest.retry = true prepUploadResponse = try await SmileID.api.prepUpload( - request: prepUploadRequest.copy(retry: "true") + request: prepUploadRequest ) default: throw error