/ secdxctests / SFCredentialStoreTests.m
SFCredentialStoreTests.m
1 /* 2 * Copyright (c) 2018 Apple Inc. All Rights Reserved. 3 * 4 * @APPLE_LICENSE_HEADER_START@ 5 * 6 * This file contains Original Code and/or Modifications of Original Code 7 * as defined in and that are subject to the Apple Public Source License 8 * Version 2.0 (the 'License'). You may not use this file except in 9 * compliance with the License. Please obtain a copy of the License at 10 * http://www.opensource.apple.com/apsl/ and read it before using this 11 * file. 12 * 13 * The Original Code and all software distributed under the License are 14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER 15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, 16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. 18 * Please see the License for the specific language governing rights and 19 * limitations under the License. 20 * 21 * @APPLE_LICENSE_HEADER_END@ 22 */ 23 24 #import "KeychainXCTest.h" 25 #import "SFKeychainServer.h" 26 #import "SecCDKeychain.h" 27 #import "SecFileLocations.h" 28 #import <Foundation/Foundation.h> 29 #import <Foundation/NSXPCConnection_Private.h> 30 #import <XCTest/XCTest.h> 31 #import <SecurityFoundation/SFKeychain.h> 32 #import <OCMock/OCMock.h> 33 34 #if USE_KEYSTORE 35 36 @interface SFCredentialStore (UnitTestingForwardDeclarations) 37 38 - (instancetype)_init; 39 40 - (id<NSXPCProxyCreating>)_serverConnectionWithError:(NSError**)error; 41 42 @end 43 44 @interface SFKeychainServer (UnitTestingForwardDeclarations) 45 46 @property (readonly, getter=_keychain) SecCDKeychain* keychain; 47 48 @end 49 50 @interface SFKeychainServerConnection (UnitTestingRedeclarations) 51 52 - (instancetype)initWithKeychain:(SecCDKeychain*)keychain xpcConnection:(NSXPCConnection*)connection; 53 54 @end 55 56 @interface SecCDKeychain (UnitTestingRedeclarations) 57 58 - (NSData*)_onQueueGetDatabaseKeyDataWithError:(NSError**)error; 59 60 @end 61 62 @interface KeychainNoXPCServerProxy : NSObject <NSXPCProxyCreating> 63 64 @property (readonly) SFKeychainServer* server; 65 66 @end 67 68 @implementation KeychainNoXPCServerProxy { 69 SFKeychainServer* _server; 70 SFKeychainServerFakeConnection* _connection; 71 } 72 73 @synthesize server = _server; 74 75 - (instancetype)init 76 { 77 if (self = [super init]) { 78 NSURL* persistentStoreURL = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"CDKeychain"); 79 NSBundle* resourcesBundle = [NSBundle bundleWithPath:@"/System/Library/Keychain/KeychainResources.bundle"]; 80 NSURL* managedObjectModelURL = [resourcesBundle URLForResource:@"KeychainModel" withExtension:@"momd"]; 81 _server = [[SFKeychainServer alloc] initWithStorageURL:persistentStoreURL modelURL:managedObjectModelURL encryptDatabase:false]; 82 _connection = [[SFKeychainServerFakeConnection alloc] initWithKeychain:_server.keychain xpcConnection:nil]; 83 } 84 85 return self; 86 } 87 88 - (id)remoteObjectProxy 89 { 90 return _server; 91 } 92 93 - (id)remoteObjectProxyWithErrorHandler:(void (^)(NSError*))handler 94 { 95 return _connection; 96 } 97 98 @end 99 100 @interface SFCredentialStoreTests : KeychainXCTest 101 @end 102 103 @implementation SFCredentialStoreTests { 104 SFCredentialStore* _credentialStore; 105 } 106 107 + (void)setUp 108 { 109 [super setUp]; 110 111 id credentialStoreMock = OCMClassMock([SFCredentialStore class]); 112 [[[[credentialStoreMock stub] andCall:@selector(serverProxyWithError:) onObject:self] ignoringNonObjectArgs] _serverConnectionWithError:NULL]; 113 } 114 115 + (id)serverProxyWithError:(NSError**)error 116 { 117 return [[KeychainNoXPCServerProxy alloc] init]; 118 } 119 120 - (void)setUp 121 { 122 [super setUp]; 123 self.keychainPartialMock = OCMPartialMock([(SFKeychainServer*)[[self.class serverProxyWithError:nil] server] _keychain]); 124 [[[[self.keychainPartialMock stub] andCall:@selector(getDatabaseKeyDataWithError:) onObject:self] ignoringNonObjectArgs] _onQueueGetDatabaseKeyDataWithError:NULL]; 125 126 _credentialStore = [[SFCredentialStore alloc] _init]; 127 } 128 129 - (BOOL)passwordCredential:(SFPasswordCredential*)firstCredential matchesCredential:(SFPasswordCredential*)secondCredential 130 { 131 return [firstCredential.primaryServiceIdentifier isEqual:secondCredential.primaryServiceIdentifier] && 132 [[NSSet setWithArray:firstCredential.supplementaryServiceIdentifiers] isEqualToSet:[NSSet setWithArray:secondCredential.supplementaryServiceIdentifiers]] && 133 [firstCredential.localizedLabel isEqualToString:secondCredential.localizedLabel] && 134 [firstCredential.localizedDescription isEqualToString:secondCredential.localizedDescription] && 135 [firstCredential.customAttributes isEqualToDictionary:secondCredential.customAttributes]; 136 } 137 138 #pragma clang diagnostic push 139 #pragma clang diagnostic ignored "-Warc-retain-cycles" 140 // we don't care about creating retain cycles inside our testing blocks (they get broken properly anyway) 141 142 - (void)testAddAndFetchCredential 143 { 144 SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]]; 145 __block NSString* credentialIdentifier = nil; 146 147 XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"]; 148 [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 149 credentialIdentifier = persistentIdentifier; 150 XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential"); 151 XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error); 152 [addExpectation fulfill]; 153 }]; 154 [self waitForExpectations:@[addExpectation] timeout:5.0]; 155 156 XCTestExpectation* fetchExpecation = [self expectationWithDescription:@"fetch credential"]; 157 [_credentialStore fetchPasswordCredentialForPersistentIdentifier:credentialIdentifier withResultHandler:^(SFPasswordCredential* fetchedCredential, NSString* password, NSError* error) { 158 XCTAssertNotNil(fetchedCredential, @"failed to fetch credential just added to store"); 159 XCTAssertNil(error, @"received unexpected error fetching credential from store: %@", error); 160 XCTAssertTrue([self passwordCredential:credential matchesCredential:fetchedCredential], @"the credential we fetched from the store does not match the one we added"); 161 XCTAssertEqualObjects(password, @"TestPass", @"the password we fetched from the store does not match the one we added"); 162 [fetchExpecation fulfill]; 163 }]; 164 [self waitForExpectations:@[fetchExpecation] timeout:5.0]; 165 } 166 167 - (void)testLookupCredential 168 { 169 SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]]; 170 171 XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"]; 172 [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 173 XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential"); 174 XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error); 175 [addExpectation fulfill]; 176 }]; 177 [self waitForExpectations:@[addExpectation] timeout:5.0]; 178 179 SFServiceIdentifier* serviceIdentifier = [SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]; 180 if (!serviceIdentifier) { 181 XCTAssertTrue(false, @"Failed to create a service identifier; aborting test"); 182 return; 183 } 184 185 XCTestExpectation* lookupExpecation = [self expectationWithDescription:@"lookup credential"]; 186 [_credentialStore lookupCredentialsForServiceIdentifiers:@[serviceIdentifier] withResultHandler:^(NSArray<SFCredential*>* results, NSError* error) { 187 XCTAssertEqual((int)results.count, 1, @"error looking up credentials with service identifiers; expected 1 result but got %d", (int)results.count); 188 XCTAssertTrue([self passwordCredential:credential matchesCredential:(SFPasswordCredential*)results.firstObject], @"the credential we looked up does not match the one we added"); 189 [lookupExpecation fulfill]; 190 }]; 191 [self waitForExpectations:@[lookupExpecation] timeout:5.0]; 192 } 193 194 - (void)testAddDuplicateCredential 195 { 196 SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]]; 197 198 XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"]; 199 [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 200 XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential"); 201 XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error); 202 [addExpectation fulfill]; 203 }]; 204 [self waitForExpectations:@[addExpectation] timeout:5.0]; 205 206 XCTestExpectation* conflictingAddExpectation = [self expectationWithDescription:@"add conflicting item"]; 207 SFCredential* conflictingCredential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"DifferentPassword" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]]; 208 [_credentialStore addCredential:conflictingCredential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 209 XCTAssertNil(persistentIdentifier, @"adding a credential seems to have succeeded when we expected it to fail"); 210 XCTAssertNotNil(error, @"failed to get error when adding a credential that should be rejected as a duplicate entry"); 211 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"duplicate error domain is not SFKeychainErrorDomain"); 212 XCTAssertEqual(error.code, SFKeychainErrorDuplicateItem, @"duplicate error is not SFKeychainErrorDuplicateItem"); 213 [conflictingAddExpectation fulfill]; 214 }]; 215 [self waitForExpectations:@[conflictingAddExpectation] timeout:5.0]; 216 } 217 218 - (void)testRemoveCredential 219 { 220 SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]]; 221 222 __block NSString* newItemPersistentIdentifier = nil; 223 XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"]; 224 [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 225 XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential"); 226 XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error); 227 228 newItemPersistentIdentifier = persistentIdentifier; 229 [addExpectation fulfill]; 230 }]; 231 [self waitForExpectations:@[addExpectation] timeout:5.0]; 232 233 XCTestExpectation* removeExpectation = [self expectationWithDescription:@"remove credential"]; 234 [_credentialStore removeCredentialWithPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(BOOL success, NSError* error) { 235 XCTAssertTrue(success, @"failed to remove credential from store"); 236 XCTAssertNil(error, @"encountered error attempting to remove credential from store: %@", error); 237 [removeExpectation fulfill]; 238 }]; 239 [self waitForExpectations:@[removeExpectation] timeout:5.0]; 240 241 XCTestExpectation* removeAgainExpectation = [self expectationWithDescription:@"remove credential gain"]; 242 [_credentialStore removeCredentialWithPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(BOOL success, NSError* error) { 243 XCTAssertFalse(success, @"somehow succeeded at removing a credential that we'd already deleted"); 244 XCTAssertNotNil(error, @"failed to get an error attempting to remove credential from store when there should not be a credential to delete"); 245 [removeAgainExpectation fulfill]; 246 }]; 247 248 XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch credential"]; 249 [_credentialStore fetchPasswordCredentialForPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(SFPasswordCredential* fetchedCredential, NSString* password, NSError* error) { 250 XCTAssertNil(fetchedCredential, @"found credential that we expected to be deleted"); 251 XCTAssertNil(password, @"found password when credential was supposed to be deleted"); 252 XCTAssertNotNil(error, "failed to get an error when fetching deleted credential"); 253 [fetchExpectation fulfill]; 254 }]; 255 [self waitForExpectations:@[removeAgainExpectation, fetchExpectation] timeout:5.0]; 256 257 // now try adding the thing again to make sure that works 258 XCTestExpectation* addAgainExpectation = [self expectationWithDescription:@"add credential again"]; 259 [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 260 XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential"); 261 XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error); 262 XCTAssertNotEqual(persistentIdentifier, newItemPersistentIdentifier, @"the added credential has the same persistent identifier as the item we already deleted"); 263 264 newItemPersistentIdentifier = persistentIdentifier; 265 [addAgainExpectation fulfill]; 266 }]; 267 [self waitForExpectations:@[addAgainExpectation] timeout:5.0]; 268 269 XCTestExpectation* fetchAgainExpectation = [self expectationWithDescription:@"fetch credential again"]; 270 [_credentialStore fetchPasswordCredentialForPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(SFPasswordCredential* fetchedCredential, NSString* password, NSError* error) { 271 XCTAssertNotNil(fetchedCredential, @"failed to fetch credential just added to store"); 272 XCTAssertNil(error, @"received unexpected error fetching credential from store: %@", error); 273 XCTAssertTrue([self passwordCredential:credential matchesCredential:fetchedCredential], @"the credential we fetched from the store does not match the one we added"); 274 XCTAssertEqualObjects(password, @"TestPass", @"the password we fetched from the store does not match the one we added"); 275 [fetchAgainExpectation fulfill]; 276 }]; 277 [self waitForExpectations:@[fetchAgainExpectation] timeout:5.0]; 278 } 279 280 - (void)testRemoveCredentialWithBadPersistentIdentifier 281 { 282 SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]]; 283 284 __block NSString* newItemPersistentIdentifier = nil; 285 XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"]; 286 [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) { 287 XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential"); 288 XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error); 289 290 newItemPersistentIdentifier = persistentIdentifier; 291 [addExpectation fulfill]; 292 }]; 293 [self waitForExpectations:@[addExpectation] timeout:5.0]; 294 295 NSString* wrongPersistentIdentifier = [[NSUUID UUID] UUIDString]; 296 XCTestExpectation* removeWrongIdentifierEsxpectation = [self expectationWithDescription:@"remove wrong persistent identifier"]; 297 [_credentialStore removeCredentialWithPersistentIdentifier:wrongPersistentIdentifier withResultHandler:^(BOOL success, NSError* error) { 298 XCTAssertFalse(success, @"reported success deleting a credential that was never there"); 299 XCTAssertNotNil(error, @"failed to get error when attempting to delete an item with an erroneous persistent identifier"); 300 [removeWrongIdentifierEsxpectation fulfill]; 301 }]; 302 303 NSString* notEvenAUUIDString = @"badstring"; 304 XCTestExpectation* removeNonUUIDIdentifierExpectation = [self expectationWithDescription:@"remove non-uuid string identifier"]; 305 [_credentialStore removeCredentialWithPersistentIdentifier:notEvenAUUIDString withResultHandler:^(BOOL success, NSError* error) { 306 XCTAssertFalse(success, @"reported success deleting a credential with a malformed persistent identifier"); 307 XCTAssertNotNil(error, @"failed to get error when attempting to delete an item with a malformed persistent identifier"); 308 XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain); 309 XCTAssertEqual(error.code, SFKeychainErrorInvalidPersistentIdentifier); 310 [removeNonUUIDIdentifierExpectation fulfill]; 311 }]; 312 [self waitForExpectations:@[removeWrongIdentifierEsxpectation, removeNonUUIDIdentifierExpectation] timeout:5.0]; 313 } 314 315 #pragma clang diagnostic pop 316 317 @end 318 319 #endif