diff --git a/Locksmith.podspec b/Locksmith.podspec index d341801..efd9d6b 100644 --- a/Locksmith.podspec +++ b/Locksmith.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Locksmith" - s.version = "2.0.2" + s.version = "2.0.7" s.summary = "Locksmith is a powerful, protocol-oriented library for working with the keychain in Swift." s.description = <<-DESC Locksmith is a powerful, protocol-oriented library for working with the iOS, Mac OS X, watchOS, and tvOS keychain in Swift. It provides extensive support for a lot of different keychain requests, and extensively uses Swift-native concepts. diff --git a/Source/Locksmith.swift b/Source/Locksmith.swift index 365e1fc..de4b089 100644 --- a/Source/Locksmith.swift +++ b/Source/Locksmith.swift @@ -39,15 +39,14 @@ public struct Locksmith { } public static func updateData(data: [String: AnyObject], forUserAccount userAccount: String, inService service: String = LocksmithDefaultService) throws { - // Delete and then re-save - do { - try Locksmith.deleteDataForUserAccount(userAccount, inService: service) - } catch { - // Deletion is likely to fail if the piece of data doesn't exist yet. - // This doesn't matter--we only tell the user about errors on the save request. + struct UpdateRequest: GenericPasswordSecureStorable, CreateableSecureStorable { + let service: String + let account: String + let data: [String: AnyObject] } - - return try Locksmith.saveData(data, forUserAccount: userAccount, inService: service) + + let request = UpdateRequest(service: service, account: userAccount, data: data) + try request.updateInSecureStore() } } @@ -424,6 +423,7 @@ public protocol CreateableSecureStorable: SecureStorable { var data: [String: AnyObject] { get } var performCreateRequestClosure: PerformRequestClosureType { get } func createInSecureStore() throws + func updateInSecureStore() throws } // MARK: - ReadableSecureStorable @@ -509,6 +509,23 @@ public protocol DeleteableSecureStorable: SecureStorable { // MARK: - Default property dictionaries +extension CreateableSecureStorable { + func updateInSecureStore(query: [String: AnyObject]) throws { + var attributesToUpdate = query + attributesToUpdate[String(kSecClass)] = nil + + let status = SecItemUpdate(query, attributesToUpdate) + + if let error = LocksmithError(fromStatusCode: Int(status)) { + throw error + } + + if status != errSecSuccess { + throw LocksmithError.Undefined + } + } +} + public extension CreateableSecureStorable where Self : GenericPasswordSecureStorable { var asCreateableSecureStoragePropertyDictionary: [String: AnyObject] { var old = genericPasswordBaseStoragePropertyDictionary @@ -521,6 +538,9 @@ public extension CreateableSecureStorable where Self : GenericPasswordSecureStor func createInSecureStore() throws { try performSecureStorageAction(performCreateRequestClosure, secureStoragePropertyDictionary: asCreateableSecureStoragePropertyDictionary) } + func updateInSecureStore() throws { + try self.updateInSecureStore(self.asCreateableSecureStoragePropertyDictionary) + } } public extension CreateableSecureStorable where Self : InternetPasswordSecureStorable { @@ -543,6 +563,9 @@ public extension CreateableSecureStorable where Self : InternetPasswordSecureSto func createInSecureStore() throws { try performSecureStorageAction(performCreateRequestClosure, secureStoragePropertyDictionary: asCreateableSecureStoragePropertyDictionary) } + func updateInSecureStore() throws { + try self.updateInSecureStore(self.asCreateableSecureStoragePropertyDictionary) + } } public extension DeleteableSecureStorable { diff --git a/Tests/LocksmithTests.swift b/Tests/LocksmithTests.swift index 719537e..8042aeb 100644 --- a/Tests/LocksmithTests.swift +++ b/Tests/LocksmithTests.swift @@ -94,6 +94,24 @@ class LocksmithTests: XCTestCase { createGenericPasswordWithData(data) } + func testUpdateForGenericPassword() { + let data = ["some": "data"] + + struct CreateGenericPassword: CreateableSecureStorable, GenericPasswordSecureStorable, ReadableSecureStorable { + var data: [String: AnyObject] + let account: String + let service: String + } + + var create = CreateGenericPassword(data: data, account: userAccount, service: service) + try! create.createInSecureStore() // make sure it doesn't throw + create.data = ["other": "data"] + try! create.updateInSecureStore() + + let read = create.readFromSecureStore()!.data as! [String: String] + XCTAssertEqual(read, ["other": "data"]) + } + func testLoadForGenericPassword() { let data = ["one": "two"] createGenericPasswordWithData(data) @@ -239,7 +257,7 @@ class LocksmithTests: XCTestCase { func testInternetPasswordMetaAttributesAreCreatedAndReturned() { struct CreateInternetPassword: CreateableSecureStorable, InternetPasswordSecureStorable { let account: String - let data: [String: AnyObject] + var data: [String: AnyObject] let server: String let port: Int let internetProtocol: LocksmithInternetProtocol @@ -265,20 +283,31 @@ class LocksmithTests: XCTestCase { let authenticationType: LocksmithInternetAuthenticationType } - let c = CreateInternetPassword(account: userAccount, data: initialData, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType, path: path, securityDomain: securityDomain) + var c = CreateInternetPassword(account: userAccount, data: initialData, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType, path: path, securityDomain: securityDomain) try! c.createInSecureStore() + + func assertResultMetadataIsOk(result: InternetPasswordSecureStorableResultType?) { + XCTAssertEqual(result?.account, userAccount) + XCTAssertEqual(result?.server, server) + XCTAssertEqual(result?.port, port) + XCTAssertEqual(result?.internetProtocol, internetProtocol) + XCTAssertEqual(result?.authenticationType, authenticationType) + XCTAssertEqual(result?.securityDomain, securityDomain) + XCTAssertEqual(result?.path, path) + } let r = ReadInternetPassword(account: userAccount, server: server, port: port, internetProtocol: internetProtocol, authenticationType: authenticationType) let result = r.readFromSecureStore() - - XCTAssertEqual(result?.account, userAccount) XCTAssertEqual(result!.data as! [String: String], initialData) - XCTAssertEqual(result?.server, server) - XCTAssertEqual(result?.port, port) - XCTAssertEqual(result?.internetProtocol, internetProtocol) - XCTAssertEqual(result?.authenticationType, authenticationType) - XCTAssertEqual(result?.securityDomain, securityDomain) - XCTAssertEqual(result?.path, path) + assertResultMetadataIsOk(result) + + // Assert that metadata is maintained after an update + c.data = ["other internet": "junk"] + try! c.updateInSecureStore() + + let result2 = r.readFromSecureStore() + XCTAssertEqual(result2!.data as! [String: String], ["other internet": "junk"]) + assertResultMetadataIsOk(result2) } func assertStringPairsMatchInDictionary(dictionary: NSDictionary, pairs: [(key: CFString, expectedOutput: String)]) {