diff --git a/.gitignore b/.gitignore index 9d1223c..4b1ee19 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Figo.xcworkspace/xcuserdata Carthage Paw build +Figo.xcodeproj/project.xcworkspace/xcuserdata diff --git a/Figo.podspec b/Figo.podspec index 6eac7b9..176eab7 100644 --- a/Figo.podspec +++ b/Figo.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| s.name = "Figo" - s.version = "2.0.1" + s.version = "2.0.2" s.summary = "Wraps the figo Connect API endpoints in nicely typed Swift functions and types for your conveniece." s.description = <<-DESC The figo Connect API allows you to easily access your bank account including transaction history and submitting payments. @@ -25,6 +25,6 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/figo-connect/ios-sdk.git", :tag => "#{s.version}" } s.source_files = "Source/**/*.swift" - s.resource = "api.figo.me.cer" + s.resources = "*.cer" end diff --git a/Figo.xcodeproj/project.pbxproj b/Figo.xcodeproj/project.pbxproj index 7876685..e7c49ec 100644 --- a/Figo.xcodeproj/project.pbxproj +++ b/Figo.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 7B6025731EF169E2005EF5B0 /* figo_2017.cer in Resources */ = {isa = PBXBuildFile; fileRef = 7B6025721EF169E2005EF5B0 /* figo_2017.cer */; }; + 7B6025741EF169E2005EF5B0 /* figo_2017.cer in Resources */ = {isa = PBXBuildFile; fileRef = 7B6025721EF169E2005EF5B0 /* figo_2017.cer */; }; + 7B6025751EF169E7005EF5B0 /* figo_2017.cer in Resources */ = {isa = PBXBuildFile; fileRef = 7B6025721EF169E2005EF5B0 /* figo_2017.cer */; }; + 7B6025761EF169E8005EF5B0 /* figo_2017.cer in Resources */ = {isa = PBXBuildFile; fileRef = 7B6025721EF169E2005EF5B0 /* figo_2017.cer */; }; 830E63D91C05A4050048F7BF /* TANScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830E63D81C05A4050048F7BF /* TANScheme.swift */; }; 830E63DE1C05A7070048F7BF /* SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830E63DD1C05A7070048F7BF /* SyncStatus.swift */; }; 830E63E31C05AB890048F7BF /* PaymentParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830E63E21C05AB890048F7BF /* PaymentParameters.swift */; }; @@ -16,10 +20,10 @@ 831183761E3DF13D000DA80C /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8311836F1E3DF136000DA80C /* Logging.swift */; }; 831183771E3DF141000DA80C /* URLRequest+curlCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831183701E3DF136000DA80C /* URLRequest+curlCommand.swift */; }; 831183781E3DF141000DA80C /* URLRequest+curlCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 831183701E3DF136000DA80C /* URLRequest+curlCommand.swift */; }; - 831183B81E3E2F66000DA80C /* api.figo.me.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* api.figo.me.cer */; }; - 831183B91E3E2F66000DA80C /* api.figo.me.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* api.figo.me.cer */; }; - 831183BA1E3E2F66000DA80C /* api.figo.me.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* api.figo.me.cer */; }; - 831183BB1E3E2F66000DA80C /* api.figo.me.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* api.figo.me.cer */; }; + 831183B81E3E2F66000DA80C /* figo_2016.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* figo_2016.cer */; }; + 831183B91E3E2F66000DA80C /* figo_2016.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* figo_2016.cer */; }; + 831183BA1E3E2F66000DA80C /* figo_2016.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* figo_2016.cer */; }; + 831183BB1E3E2F66000DA80C /* figo_2016.cer in Resources */ = {isa = PBXBuildFile; fileRef = 831183B71E3E2F66000DA80C /* figo_2016.cer */; }; 832CABAE1E242F8700D48895 /* Unbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832CABAD1E242F8700D48895 /* Unbox.swift */; }; 833C01001E2AA72A00AA5E7C /* Figo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 833C00EF1E2AA6A100AA5E7C /* Figo.framework */; }; 833C01061E2AA7F400AA5E7C /* FigoClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83F0E46B1C08A91400FB3709 /* FigoClient.swift */; }; @@ -179,6 +183,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 7B6025721EF169E2005EF5B0 /* figo_2017.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = figo_2017.cer; sourceTree = ""; }; 83017B771C0A2FE80062FC08 /* TaskState.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = TaskState.json; path = Resources/TaskState.json; sourceTree = ""; }; 830E63D81C05A4050048F7BF /* TANScheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TANScheme.swift; path = Types/TANScheme.swift; sourceTree = ""; }; 830E63DB1C05A4960048F7BF /* TanScheme.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = TanScheme.json; path = Resources/TanScheme.json; sourceTree = ""; }; @@ -188,7 +193,7 @@ 831183651E3DEAE1000DA80C /* ServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTests.swift; sourceTree = ""; }; 8311836F1E3DF136000DA80C /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; 831183701E3DF136000DA80C /* URLRequest+curlCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+curlCommand.swift"; sourceTree = ""; }; - 831183B71E3E2F66000DA80C /* api.figo.me.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = api.figo.me.cer; sourceTree = ""; }; + 831183B71E3E2F66000DA80C /* figo_2016.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = figo_2016.cer; sourceTree = ""; }; 832CABAD1E242F8700D48895 /* Unbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Unbox.swift; sourceTree = ""; }; 833285EE1C04D45900A9FE73 /* Balance.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = Balance.json; path = Resources/Balance.json; sourceTree = ""; }; 833285F01C04D48E00A9FE73 /* Resources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resources.swift; sourceTree = ""; }; @@ -454,7 +459,8 @@ isa = PBXGroup; children = ( 83D3A7201C03471D003EDE45 /* README.md */, - 831183B71E3E2F66000DA80C /* api.figo.me.cer */, + 831183B71E3E2F66000DA80C /* figo_2016.cer */, + 7B6025721EF169E2005EF5B0 /* figo_2017.cer */, 83D3A67F1BFF2953003EDE45 /* Source */, 83D3A7081BFFB6A7003EDE45 /* Tests */, 83F3AF231BFF28D900767D77 /* Products */, @@ -571,7 +577,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 0830; ORGANIZATIONNAME = CodeStage; TargetAttributes = { 833C00EE1E2AA6A100AA5E7C = { @@ -584,11 +590,12 @@ }; 833C01391E2AAB4200AA5E7C = { CreatedOnToolsVersion = 8.2.1; + LastSwiftMigration = 0830; ProvisioningStyle = Manual; }; 83F3AF211BFF28D900767D77 = { CreatedOnToolsVersion = 7.1.1; - LastSwiftMigration = 0820; + LastSwiftMigration = 0830; ProvisioningStyle = Manual; }; }; @@ -618,7 +625,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 831183BA1E3E2F66000DA80C /* api.figo.me.cer in Resources */, + 831183BA1E3E2F66000DA80C /* figo_2016.cer in Resources */, + 7B6025741EF169E2005EF5B0 /* figo_2017.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -626,7 +634,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 831183BB1E3E2F66000DA80C /* api.figo.me.cer in Resources */, + 831183BB1E3E2F66000DA80C /* figo_2016.cer in Resources */, 833C01991E2AADB400AA5E7C /* Account.json in Resources */, 833C019A1E2AADB400AA5E7C /* User.json in Resources */, 833C019B1E2AADB400AA5E7C /* Balance.json in Resources */, @@ -640,6 +648,7 @@ 833C01A31E2AADB400AA5E7C /* Security.json in Resources */, 833C01A41E2AADB400AA5E7C /* StandingOrder.json in Resources */, 833C01A51E2AADB400AA5E7C /* Payment.json in Resources */, + 7B6025761EF169E8005EF5B0 /* figo_2017.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -647,7 +656,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 831183B91E3E2F66000DA80C /* api.figo.me.cer in Resources */, + 831183B91E3E2F66000DA80C /* figo_2016.cer in Resources */, 833C01511E2AAC0600AA5E7C /* Account.json in Resources */, 833C01521E2AAC0600AA5E7C /* User.json in Resources */, 833C01531E2AAC0600AA5E7C /* Balance.json in Resources */, @@ -661,6 +670,7 @@ 833C015B1E2AAC0600AA5E7C /* Security.json in Resources */, 833C015C1E2AAC0600AA5E7C /* StandingOrder.json in Resources */, 833C015D1E2AAC0600AA5E7C /* Payment.json in Resources */, + 7B6025751EF169E7005EF5B0 /* figo_2017.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -668,7 +678,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 831183B81E3E2F66000DA80C /* api.figo.me.cer in Resources */, + 831183B81E3E2F66000DA80C /* figo_2016.cer in Resources */, + 7B6025731EF169E2005EF5B0 /* figo_2017.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -982,6 +993,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -1032,6 +1044,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -1066,6 +1079,7 @@ PRODUCT_BUNDLE_IDENTIFIER = io.figo.iOS; PRODUCT_NAME = Figo; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -1081,6 +1095,7 @@ PRODUCT_BUNDLE_IDENTIFIER = io.figo.iOS; PRODUCT_NAME = Figo; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/Figo.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate b/Figo.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index cc912f5..0000000 Binary files a/Figo.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/Figo.xcodeproj/xcshareddata/xcschemes/Figo iOS.xcscheme b/Figo.xcodeproj/xcshareddata/xcschemes/Figo iOS.xcscheme index 648cabc..152137a 100644 --- a/Figo.xcodeproj/xcshareddata/xcschemes/Figo iOS.xcscheme +++ b/Figo.xcodeproj/xcshareddata/xcschemes/Figo iOS.xcscheme @@ -1,6 +1,6 @@ (_ data: FigoResult, co internal func base64EncodeBasicAuthCredentials(_ clientID: String, _ clientSecret: String) -> String { let clientCode: String = clientID + ":" + clientSecret let utf8str: Data = clientCode.data(using: String.Encoding.utf8)! - return utf8str.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn) + return utf8str.base64EncodedString(options: Data.Base64EncodingOptions.endLineWithCarriageReturn) } diff --git a/Source/FigoClient.swift b/Source/FigoClient.swift index 482b422..04911b9 100644 --- a/Source/FigoClient.swift +++ b/Source/FigoClient.swift @@ -16,7 +16,7 @@ internal let POLLING_INTERVAL_MSECS: Int64 = Int64(400) * Int64(NSEC_PER_MSEC) internal let POLLING_COUNTDOWN_INITIAL_VALUE = 100 // 100 x 400 ms = 40 s /// Name of certificate file for public key pinning -internal let CERTIFICATE_FILE = "api.figo.me" +internal let CERTIFICATE_FILES = ["figo_2016", "figo_2017"] /** @@ -33,12 +33,12 @@ internal let CERTIFICATE_FILE = "api.figo.me" */ public class FigoClient: NSObject { - private lazy var session: URLSession = { + fileprivate lazy var session: URLSession = { return URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) }() /// Used for Basic HTTP authentication, derived from CliendID and ClientSecret - private var basicAuthCredentials: String? + fileprivate var basicAuthCredentials: String? /// OAuth2 access token var accessToken: String? @@ -46,14 +46,9 @@ public class FigoClient: NSObject { /// OAuth2 refresh token var refreshToken: String? - /// Public key extracted from certificate file in bundle - lazy var publicKey: SecKey = { - let url = Bundle(for: FigoClient.self).url(forResource: CERTIFICATE_FILE, withExtension: "cer")! - let data = try? Data(contentsOf: url) - assert(data != nil, "Failed to load contents of certificate file '\(CERTIFICATE_FILE).cer'") - let key = publicKeyForCertificateData(data: data!) - assert(key != nil, "Failed to extract public key from certificate file '\(CERTIFICATE_FILE).cer'") - return key! + /// Public keys extracted from certificate files in bundle + lazy var publicKeys: [SecKey] = { + return publicKeysForResources(CERTIFICATE_FILES) }() @@ -155,25 +150,28 @@ public class FigoClient: NSObject { Checks the server's certificates to make sure that you are really talking to the figo server */ public func dispositionForChallenge(_ challenge: URLAuthenticationChallenge) -> URLSession.AuthChallengeDisposition { - var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling - if let serverTrust = challenge.protectionSpace.serverTrust { + if !trustIsValid(serverTrust) { + return .cancelAuthenticationChallenge + } + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { - let serverKeys = publicKeysForServerTrust(serverTrust: serverTrust) as NSArray - if !serverKeys.contains(self.publicKey) { - disposition = .cancelAuthenticationChallenge + let serverKeys = publicKeysForServerTrust(serverTrust) as NSArray + for clientKey in self.publicKeys { + if serverKeys.contains(clientKey) { + return .performDefaultHandling + } } + return .cancelAuthenticationChallenge } - if !trustIsValid(serverTrust) { - disposition = .cancelAuthenticationChallenge - } + } else { if challenge.previousFailureCount > 0 { - disposition = .cancelAuthenticationChallenge + return .cancelAuthenticationChallenge } } - return disposition + return .performDefaultHandling } } @@ -183,7 +181,7 @@ extension FigoClient: URLSessionDelegate { } } -private func publicKeyForCertificateData(data: Data) -> SecKey? { +private func publicKeyForCertificateData(_ data: Data) -> SecKey? { if let certificate = SecCertificateCreateWithData(nil, data as CFData) { var trust: SecTrust? let status = SecTrustCreateWithCertificates(certificate, SecPolicyCreateBasicX509(), &trust) @@ -197,7 +195,7 @@ private func publicKeyForCertificateData(data: Data) -> SecKey? { return nil } -private func publicKeysForServerTrust(serverTrust: SecTrust) -> [SecKey] { +private func publicKeysForServerTrust(_ serverTrust: SecTrust) -> [SecKey] { var keys: [SecKey] = [] for index in 0 ..< SecTrustGetCertificateCount(serverTrust) { @@ -229,3 +227,20 @@ private func trustIsValid(_ trust: SecTrust) -> Bool { } return isValid } + +private func publicKeysForResources(_ resources: [String]) -> [SecKey] { + var publicKeys: [SecKey] = [] + + for resource in resources { + let url = Bundle(for: FigoClient.self).url(forResource: resource, withExtension: "cer")! + let data = try? Data(contentsOf: url) + assert(data != nil, "Failed to load contents of certificate file '\(resource).cer'") + let key = publicKeyForCertificateData(data!) + assert(key != nil, "Failed to extract public key from certificate file '\(resource).cer'") + if let key = key { + publicKeys.append(key) + } + } + + return publicKeys +} diff --git a/Source/Logging/URLRequest+curlCommand.swift b/Source/Logging/URLRequest+curlCommand.swift index 37ec5e1..21495b0 100644 --- a/Source/Logging/URLRequest+curlCommand.swift +++ b/Source/Logging/URLRequest+curlCommand.swift @@ -31,7 +31,7 @@ extension URLRequest { command.append(" -H \"\(key): \(value)\"") } command.append(" --compressed") - command.append(" \"\(self.url?.absoluteString)\"") + command.append(" \"\(self.url!.absoluteString)\"") command.append(" | python -mjson.tool") return command } diff --git a/Tests/AccountTests.swift b/Tests/AccountTests.swift index 1df5aef..3078a52 100644 --- a/Tests/AccountTests.swift +++ b/Tests/AccountTests.swift @@ -21,7 +21,7 @@ class AccountTests: BaseTestCaseWithLogin { XCTAssertGreaterThan(accounts.count, 0) print("\(accounts.count) accounts:") for account in accounts { - print("\(account.accountID) \(account.bankID) \(account.bankCode) \(account.name) \(account.balanceFormatted ?? "")") + print("\(account.accountID) \(account.bankID ?? "null") \(account.bankCode) \(account.name) \(account.balanceFormatted ?? "")") } break case .failure(let error): diff --git a/Tests/BaseTestCaseWithLogin.swift b/Tests/BaseTestCaseWithLogin.swift index be7502c..798b1df 100644 --- a/Tests/BaseTestCaseWithLogin.swift +++ b/Tests/BaseTestCaseWithLogin.swift @@ -68,7 +68,7 @@ class BaseTestCaseWithLogin: XCTestCase { } /// Allows you to get rid of the boilerplate code for async callbacks in test cases - func waitForCompletionOfTests(tests: (_ doneWaiting: @escaping () -> ()) -> ()) { + func waitForCompletionOfTests(_ tests: (_ doneWaiting: @escaping () -> ()) -> ()) { let completionExpectation = self.expectation(description: "Completion should be called") tests { completionExpectation.fulfill() @@ -77,6 +77,6 @@ class BaseTestCaseWithLogin: XCTestCase { } func testThatCertificateIsPresent() { - XCTAssertNotNil(figo.publicKey) + XCTAssertNotNil(figo.publicKeys) } } diff --git a/Tests/TransactionTests.swift b/Tests/TransactionTests.swift index 0034075..6c441d7 100644 --- a/Tests/TransactionTests.swift +++ b/Tests/TransactionTests.swift @@ -41,7 +41,7 @@ class TransactionsTests: BaseTestCaseWithLogin { if case .success(let envelope) = result { print("Retrieved \(envelope.transactions.count) transactions") for t in envelope.transactions { - print("\(t.name) \(t.amountFormatted)") + print("\(t.name ?? "null")) \(t.amountFormatted)") } figo.retrieveTransaction(envelope.transactions.last!.transactionID) { result in diff --git a/api.figo.me.cer b/figo_2016.cer similarity index 100% rename from api.figo.me.cer rename to figo_2016.cer diff --git a/figo_2017.cer b/figo_2017.cer new file mode 100644 index 0000000..ff7072d Binary files /dev/null and b/figo_2017.cer differ