diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index b76b891d7..24f6e3c85 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -241,9 +241,100 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, // MARK: - API - public func storeCredentials(_ credentials: Credentials) { - self.username = credentials.username -// self.password = password + public func storeCredentials(_ credentials: Credentials) throws { + + guard let username = credentials.username, let password = credentials.password, let server = delegate.server else { + throw CredentialsError.incompleteCredentials + } + + self.username = username + + let passwordData = password.data(using: String.Encoding.utf8)! + + let updateQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrAccount as String: username, + kSecAttrServer as String: server] + let attributes: [String: Any] = [kSecValueData as String: passwordData] + let status = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary) + + switch status { + case errSecSuccess: + return + case errSecItemNotFound: + break + default: + throw CredentialsError.unhandledError(status: status) + } + + guard status == errSecItemNotFound else { + return + } + + let addQuery: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrAccount as String: username, + kSecAttrServer as String: server, + kSecValueData as String: passwordData] + let addStatus = SecItemAdd(addQuery as CFDictionary, nil) + if addStatus != errSecSuccess { + throw CredentialsError.unhandledError(status: status) + } + + } + + public func retrieveCredentials() throws -> Credentials? { + + guard let username = self.username, let server = delegate.server else { + return nil + } + + let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrAccount as String: username, + kSecAttrServer as String: server, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnAttributes as String: true, + kSecReturnData as String: true] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + guard status != errSecItemNotFound else { + return nil + } + + guard status == errSecSuccess else { + throw CredentialsError.unhandledError(status: status) + } + + guard let existingItem = item as? [String : Any], + let passwordData = existingItem[kSecValueData as String] as? Data, + let password = String(data: passwordData, encoding: String.Encoding.utf8) else { + return nil + } + + return BasicCredentials(username: username, password: password) + + } + + public func removeCredentials() throws { + + guard let username = self.username, let server = delegate.server else { + return + } + + let query: [String: Any] = [kSecClass as String: kSecClassInternetPassword, + kSecAttrAccount as String: username, + kSecAttrServer as String: server, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecReturnAttributes as String: true, + kSecReturnData as String: true] + + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw CredentialsError.unhandledError(status: status) + } + + self.username = nil + } public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift index 01b52dc2b..23e4d71c2 100644 --- a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift +++ b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift @@ -7,6 +7,7 @@ // import XCTest +import RSWeb @testable import Account class AccountCredentialsTest: XCTestCase { @@ -21,8 +22,67 @@ class AccountCredentialsTest: XCTestCase { TestAccountManager.shared.deleteAccount(account) } - func testExample() { + func testCreateRetrieveDelete() { + // Make sure any left over from failed tests are gone + do { + try account.removeCredentials() + } catch { + XCTFail(error.localizedDescription) + } + + var credentials: Credentials? = BasicCredentials(username: "maurice", password: "hardpasswd") + + // Store the credentials + do { + try account.storeCredentials(credentials!) + } catch { + XCTFail(error.localizedDescription) + } + + // Retrieve them + credentials = nil + do { + credentials = try account.retrieveCredentials() + } catch { + XCTFail(error.localizedDescription) + } + XCTAssertEqual("maurice", credentials!.username) + XCTAssertEqual("hardpasswd", credentials!.password) + + // Update them + credentials?.password = "easypasswd" + do { + try account.storeCredentials(credentials!) + } catch { + XCTFail(error.localizedDescription) + } + + // Retrieve them again + credentials = nil + do { + credentials = try account.retrieveCredentials() + } catch { + XCTFail(error.localizedDescription) + } + XCTAssertEqual("maurice", credentials!.username) + XCTAssertEqual("easypasswd", credentials!.password) + + // Delete them + do { + try account.removeCredentials() + } catch { + XCTFail(error.localizedDescription) + } + + // Make sure they are gone + do { + try credentials = account.retrieveCredentials() + } catch { + XCTFail(error.localizedDescription) + } + + XCTAssertNil(credentials) } } diff --git a/Mac/Preferences/Accounts/AccountsAddFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsAddFeedbinWindowController.swift index 43ae5ac76..294143824 100644 --- a/Mac/Preferences/Accounts/AccountsAddFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsAddFeedbinWindowController.swift @@ -67,9 +67,13 @@ class AccountsAddFeedbinWindowController: NSWindowController, NSTextFieldDelegat if authenticated { let account = AccountManager.shared.createAccount(type: .feedbin) - account.storeCredentials(credentials) + do { + try account.storeCredentials(credentials) + self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) + } catch { + self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") + } - self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) } else { self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") } diff --git a/submodules/RSWeb b/submodules/RSWeb index eae66d829..731711fff 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit eae66d829ff8beaf4ce0694d768bd5cb242fbacd +Subproject commit 731711fff487f923d5be8ea1f4a9c19a58f059c3