/ 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