Add encrypted credential storage

This commit is contained in:
Maurice Parker 2019-05-04 15:14:49 -05:00
parent 595db517a7
commit aaa4342494
4 changed files with 162 additions and 7 deletions

View File

@ -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<Bool, Error>) -> Void) {

View File

@ -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)
}
}

View File

@ -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")
}

@ -1 +1 @@
Subproject commit eae66d829ff8beaf4ce0694d768bd5cb242fbacd
Subproject commit 731711fff487f923d5be8ea1f4a9c19a58f059c3