SFCredentialStoreTests.m   [plain text]


/*
 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

#import "KeychainXCTest.h"
#import "SFKeychainServer.h"
#import "SecCDKeychain.h"
#import "SecFileLocations.h"
#import <Foundation/Foundation.h>
#import <Foundation/NSXPCConnection_Private.h>
#import <XCTest/XCTest.h>
#import <SecurityFoundation/SFKeychain.h>
#import <OCMock/OCMock.h>

#if USE_KEYSTORE

@interface SFCredentialStore (UnitTestingForwardDeclarations)

- (instancetype)_init;

- (id<NSXPCProxyCreating>)_serverConnectionWithError:(NSError**)error;

@end

@interface SFKeychainServer (UnitTestingForwardDeclarations)

@property (readonly, getter=_keychain) SecCDKeychain* keychain;

@end

@interface SFKeychainServerConnection (UnitTestingRedeclarations)

- (instancetype)initWithKeychain:(SecCDKeychain*)keychain xpcConnection:(NSXPCConnection*)connection;

@end

@interface SecCDKeychain (UnitTestingRedeclarations)

- (NSData*)_onQueueGetDatabaseKeyDataWithError:(NSError**)error;

@end

@interface KeychainNoXPCServerProxy : NSObject <NSXPCProxyCreating>

@property (readonly) SFKeychainServer* server;

@end

@implementation KeychainNoXPCServerProxy {
    SFKeychainServer* _server;
    SFKeychainServerFakeConnection* _connection;
}

@synthesize server = _server;

- (instancetype)init
{
    if (self = [super init]) {
        NSURL* persistentStoreURL = (__bridge_transfer NSURL*)SecCopyURLForFileInKeychainDirectory((__bridge CFStringRef)@"CDKeychain");
        NSBundle* resourcesBundle = [NSBundle bundleWithPath:@"/System/Library/Keychain/KeychainResources.bundle"];
        NSURL* managedObjectModelURL = [resourcesBundle URLForResource:@"KeychainModel" withExtension:@"momd"];
        _server = [[SFKeychainServer alloc] initWithStorageURL:persistentStoreURL modelURL:managedObjectModelURL encryptDatabase:false];
        _connection = [[SFKeychainServerFakeConnection alloc] initWithKeychain:_server.keychain xpcConnection:nil];
    }
    
    return self;
}

- (id)remoteObjectProxy
{
    return _server;
}

- (id)remoteObjectProxyWithErrorHandler:(void (^)(NSError*))handler
{
    return _connection;
}

@end

@interface SFCredentialStoreTests : KeychainXCTest
@end

@implementation SFCredentialStoreTests {
    SFCredentialStore* _credentialStore;
}

+ (void)setUp
{
    [super setUp];
    
    id credentialStoreMock = OCMClassMock([SFCredentialStore class]);
    [[[[credentialStoreMock stub] andCall:@selector(serverProxyWithError:) onObject:self] ignoringNonObjectArgs] _serverConnectionWithError:NULL];
}

+ (id)serverProxyWithError:(NSError**)error
{
    return [[KeychainNoXPCServerProxy alloc] init];
}

- (void)setUp
{
    [super setUp];
    self.keychainPartialMock = OCMPartialMock([(SFKeychainServer*)[[self.class serverProxyWithError:nil] server] _keychain]);
    [[[[self.keychainPartialMock stub] andCall:@selector(getDatabaseKeyDataithError:) onObject:self] ignoringNonObjectArgs] _onQueueGetDatabaseKeyDataWithError:NULL];

    _credentialStore = [[SFCredentialStore alloc] _init];
}

- (BOOL)passwordCredential:(SFPasswordCredential*)firstCredential matchesCredential:(SFPasswordCredential*)secondCredential
{
    return [firstCredential.primaryServiceIdentifier isEqual:secondCredential.primaryServiceIdentifier] &&
           [[NSSet setWithArray:firstCredential.supplementaryServiceIdentifiers] isEqualToSet:[NSSet setWithArray:secondCredential.supplementaryServiceIdentifiers]] &&
           [firstCredential.localizedLabel isEqualToString:secondCredential.localizedLabel] &&
           [firstCredential.localizedDescription isEqualToString:secondCredential.localizedDescription] &&
           [firstCredential.customAttributes isEqualToDictionary:secondCredential.customAttributes];
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
// we don't care about creating retain cycles inside our testing blocks (they get broken properly anyway)

- (void)testAddAndFetchCredential
{
    SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]];
    __block NSString* credentialIdentifier = nil;
    
    XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"];
    [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        credentialIdentifier = persistentIdentifier;
        XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential");
        XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error);
        [addExpectation fulfill];
    }];
    [self waitForExpectations:@[addExpectation] timeout:5.0];

    XCTestExpectation* fetchExpecation = [self expectationWithDescription:@"fetch credential"];
    [_credentialStore fetchPasswordCredentialForPersistentIdentifier:credentialIdentifier withResultHandler:^(SFPasswordCredential* fetchedCredential, NSString* password, NSError* error) {
        XCTAssertNotNil(fetchedCredential, @"failed to fetch credential just added to store");
        XCTAssertNil(error, @"received unexpected error fetching credential from store: %@", error);
        XCTAssertTrue([self passwordCredential:credential matchesCredential:fetchedCredential], @"the credential we fetched from the store does not match the one we added");
        XCTAssertEqualObjects(password, @"TestPass", @"the password we fetched from the store does not match the one we added");
        [fetchExpecation fulfill];
    }];
    [self waitForExpectations:@[fetchExpecation] timeout:5.0];
}

- (void)testLookupCredential
{
    SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]];
    
    XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"];
    [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential");
        XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error);
        [addExpectation fulfill];
    }];
    [self waitForExpectations:@[addExpectation] timeout:5.0];
    
    SFServiceIdentifier* serviceIdentifier = [SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"];
    if (!serviceIdentifier) {
        XCTAssertTrue(false, @"Failed to create a service identifier; aborting test");
        return;
    }

    XCTestExpectation* lookupExpecation = [self expectationWithDescription:@"lookup credential"];
    [_credentialStore lookupCredentialsForServiceIdentifiers:@[serviceIdentifier] withResultHandler:^(NSArray<SFCredential*>* results, NSError* error) {
        XCTAssertEqual((int)results.count, 1, @"error looking up credentials with service identifiers; expected 1 result but got %d", (int)results.count);
        XCTAssertTrue([self passwordCredential:credential matchesCredential:(SFPasswordCredential*)results.firstObject], @"the credential we looked up does not match the one we added");
        [lookupExpecation fulfill];
    }];
    [self waitForExpectations:@[lookupExpecation] timeout:5.0];
}

- (void)testAddDuplicateCredential
{
    SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]];

    XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"];
    [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential");
        XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error);
        [addExpectation fulfill];
    }];
    [self waitForExpectations:@[addExpectation] timeout:5.0];

    XCTestExpectation* conflictingAddExpectation = [self expectationWithDescription:@"add conflicting item"];
    SFCredential* conflictingCredential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"DifferentPassword" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]];
    [_credentialStore addCredential:conflictingCredential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        XCTAssertNil(persistentIdentifier, @"adding a credential seems to have succeeded when we expected it to fail");
        XCTAssertNotNil(error, @"failed to get error when adding a credential that should be rejected as a duplicate entry");
        XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain, @"duplicate error domain is not SFKeychainErrorDomain");
        XCTAssertEqual(error.code, SFKeychainErrorDuplicateItem, @"duplicate error is not SFKeychainErrorDuplicateItem");
        [conflictingAddExpectation fulfill];
    }];
    [self waitForExpectations:@[conflictingAddExpectation] timeout:5.0];
}

- (void)testRemoveCredential
{
    SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]];
    
    __block NSString* newItemPersistentIdentifier = nil;
    XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"];
    [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential");
        XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error);
        
        newItemPersistentIdentifier = persistentIdentifier;
        [addExpectation fulfill];
    }];
    [self waitForExpectations:@[addExpectation] timeout:5.0];

    XCTestExpectation* removeExpectation = [self expectationWithDescription:@"remove credential"];
    [_credentialStore removeCredentialWithPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(BOOL success, NSError* error) {
        XCTAssertTrue(success, @"failed to remove credential from store");
        XCTAssertNil(error, @"encountered error attempting to remove credential from store: %@", error);
        [removeExpectation fulfill];
    }];
    [self waitForExpectations:@[removeExpectation] timeout:5.0];

    XCTestExpectation* removeAgainExpectation = [self expectationWithDescription:@"remove credential gain"];
    [_credentialStore removeCredentialWithPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(BOOL success, NSError* error) {
        XCTAssertFalse(success, @"somehow succeeded at removing a credential that we'd already deleted");
        XCTAssertNotNil(error, @"failed to get an error attempting to remove credential from store when there should not be a credential to delete");
        [removeAgainExpectation fulfill];
    }];

    XCTestExpectation* fetchExpectation = [self expectationWithDescription:@"fetch credential"];
    [_credentialStore fetchPasswordCredentialForPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(SFPasswordCredential* fetchedCredential, NSString* password, NSError* error) {
        XCTAssertNil(fetchedCredential, @"found credential that we expected to be deleted");
        XCTAssertNil(password, @"found password when credential was supposed to be deleted");
        XCTAssertNotNil(error, "failed to get an error when fetching deleted credential");
        [fetchExpectation fulfill];
    }];
    [self waitForExpectations:@[removeAgainExpectation, fetchExpectation] timeout:5.0];
    
    // now try adding the thing again to make sure that works
    XCTestExpectation* addAgainExpectation = [self expectationWithDescription:@"add credential again"];
    [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential");
        XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error);
        XCTAssertNotEqual(persistentIdentifier, newItemPersistentIdentifier, @"the added credential has the same persistent identifier as the item we already deleted");
        
        newItemPersistentIdentifier = persistentIdentifier;
        [addAgainExpectation fulfill];
    }];
    [self waitForExpectations:@[addAgainExpectation] timeout:5.0];

    XCTestExpectation* fetchAgainExpectation = [self expectationWithDescription:@"fetch credential again"];
    [_credentialStore fetchPasswordCredentialForPersistentIdentifier:newItemPersistentIdentifier withResultHandler:^(SFPasswordCredential* fetchedCredential, NSString* password, NSError* error) {
        XCTAssertNotNil(fetchedCredential, @"failed to fetch credential just added to store");
        XCTAssertNil(error, @"received unexpected error fetching credential from store: %@", error);
        XCTAssertTrue([self passwordCredential:credential matchesCredential:fetchedCredential], @"the credential we fetched from the store does not match the one we added");
        XCTAssertEqualObjects(password, @"TestPass", @"the password we fetched from the store does not match the one we added");
        [fetchAgainExpectation fulfill];
    }];
    [self waitForExpectations:@[fetchAgainExpectation] timeout:5.0];
}

- (void)testRemoveCredentialWithBadPersistentIdentifier
{
    SFPasswordCredential* credential = [[SFPasswordCredential alloc] initWithUsername:@"TestUser" password:@"TestPass" primaryServiceIdentifier:[SFServiceIdentifier serviceIdentifierForDomain:@"testdomain.com"]];
    
    __block NSString* newItemPersistentIdentifier = nil;
    XCTestExpectation* addExpectation = [self expectationWithDescription:@"add credential"];
    [_credentialStore addCredential:credential withAccessPolicy:[[SFAccessPolicy alloc] initWithAccessibility:SFAccessibilityMakeWithMode(SFAccessibleWhenUnlocked) sharingPolicy:SFSharingPolicyWithTrustedDevices] resultHandler:^(NSString* persistentIdentifier, NSError* error) {
        XCTAssertNotNil(persistentIdentifier, @"failed to get persistent identifier for added credential");
        XCTAssertNil(error, @"received unexpected error attempting to add credential to store: %@", error);
        
        newItemPersistentIdentifier = persistentIdentifier;
        [addExpectation fulfill];
    }];
    [self waitForExpectations:@[addExpectation] timeout:5.0];
    
    NSString* wrongPersistentIdentifier = [[NSUUID UUID] UUIDString];
    XCTestExpectation* removeWrongIdentifierEsxpectation = [self expectationWithDescription:@"remove wrong persistent identifier"];
    [_credentialStore removeCredentialWithPersistentIdentifier:wrongPersistentIdentifier withResultHandler:^(BOOL success, NSError* error) {
        XCTAssertFalse(success, @"reported success deleting a credential that was never there");
        XCTAssertNotNil(error, @"failed to get error when attempting to delete an item with an erroneous persistent identifier");
        [removeWrongIdentifierEsxpectation fulfill];
    }];
    
    NSString* notEvenAUUIDString = @"badstring";
    XCTestExpectation* removeNonUUIDIdentifierExpectation = [self expectationWithDescription:@"remove non-uuid string identifier"];
    [_credentialStore removeCredentialWithPersistentIdentifier:notEvenAUUIDString withResultHandler:^(BOOL success, NSError* error) {
        XCTAssertFalse(success, @"reported success deleting a credential with a malformed persistent identifier");
        XCTAssertNotNil(error, @"failed to get error when attempting to delete an item with a malformed persistent identifier");
        XCTAssertEqualObjects(error.domain, SFKeychainErrorDomain);
        XCTAssertEqual(error.code, SFKeychainErrorInvalidPersistentIdentifier);
        [removeNonUUIDIdentifierExpectation fulfill];
    }];
    [self waitForExpectations:@[removeWrongIdentifierEsxpectation, removeNonUUIDIdentifierExpectation] timeout:5.0];
}

#pragma clang diagnostic pop

@end

#endif