Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve HaversackEphemeralStrategy ergonomics #11

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Added convenience methods for accessing mock data values on the `HaversackEphemeralStrategy`

## [1.3.0] - 2024-02-11
### Added
- Added support for visionOS.
Expand Down
335 changes: 179 additions & 156 deletions Sources/Haversack/Haversack.swift

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Sources/Haversack/HaversackStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ open class HaversackStrategy {
///
/// This should not be called directly but is used by ``Haversack/Haversack/exportItems(_:config:)`` to perform the actual exporting
/// - Parameters:
/// - item: The keys, certificates, or identities to export
/// - items: The keys, certificates, or identities to export
/// - configuration: A configuration representing all the options that can be provided to `SecItemExport`
/// - Returns: A `Data` representation of the keychain item
open func exportItems(_ items: [any KeychainExportable], configuration: KeychainExportConfig) throws -> Data {
Expand Down Expand Up @@ -232,8 +232,9 @@ open class HaversackStrategy {

/// Imports one or more keys, certificates, or identities and adds them to the keychain
/// - Parameters:
/// - item: The keys, certificates, or identities to import
/// - items: The keys, certificates, or identities to import
/// - configuration: A configuration representing all the options that can be provided to `SecItemImport`
/// - importKeychain: The keychain to import the items to
/// - Returns: An array of all the keychain items imported
open func importItems<EntityType: KeychainImportable>(_ items: Data, configuration: KeychainImportConfig<EntityType>, importKeychain: SecKeychain? = nil) throws -> [EntityType] {
var inputFormat = configuration.inputFormat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ public struct KeychainImportConfig<T: KeychainImportable> {
}

/// Make any imported keys extractable. By default keys are not extractable after import
/// - Parameter extractable: Whether or not the keychain item can be exported from its keychain (Defaults to false)
/// - Returns: A `KeychainImportConfig` struct
public func extractable() throws -> Self where T: PrivateKeyImporting {
// swiftlint:disable:next line_length
Expand Down
156 changes: 156 additions & 0 deletions Sources/HaversackMock/HaversackEphemeralStrategy+mocking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: MIT
// Copyright 2023, Jamf

import Haversack
import Security
import XCTest

extension Haversack {
/// A convenience accessor that handles typecasting the `HaversackStrategy` to a `HaversackEphemeralStrategy`
/// via XCTest's `XCTUnwrap` function.
var ephemeralStrategy: HaversackEphemeralStrategy {
get throws {
try XCTUnwrap(configuration.strategy as? HaversackEphemeralStrategy)
}
}
}

// MARK: Mock data setters
extension Haversack {
/// Mocks data for calls to `Haversack.first(where:)`
/// - Parameters:
/// - query: The query to set a mock value for
/// - mockValue: The mock value to set
public func setSearchFirstMock<T: KeychainQuerying>(where query: T, mockValue: T.Entity) throws {
let query = try makeSearchQuery(query, singleItem: true)
try ephemeralStrategy.setMock(mockValue, forQuery: query.query)
}

/// Retrieves the value for a call to `Haversack.first(where:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter query: The query to retrieve a value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getSearchFirstMock<T: KeychainQuerying>(where query: T) throws -> T.Entity? {
let query = try makeSearchQuery(query, singleItem: true)
return try ephemeralStrategy.getMockDataValue(for: query.query)
}

/// Mocks data for calls to `Haversack.search(where:)`
/// - Parameters:
/// - query: The query to set a mock value for
/// - mockValue: The mock value to set
public func setSearchMock<T: KeychainQuerying>(where query: T, mockValue: [T.Entity]) throws {
let query = try makeSearchQuery(query, singleItem: false)
try ephemeralStrategy.setMock(mockValue, forQuery: query.query)
}

/// Retrieves the value for a call to `Haversack.search(where:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter query: The query to retrieve a value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getSearchMock<T: KeychainQuerying>(where query: T) throws -> [T.Entity]? {
let query = try makeSearchQuery(query, singleItem: false)
return try ephemeralStrategy.getMockDataValue(for: query.query)
}

/// Mocks data for calls to `Haversack.save(_:itemSecurity:updateExisting:)`. This is useful when
/// you want to test the behavior of your code when the item being saved already exists in the keychain.
/// - Parameters:
/// - item: The item being saved
/// - itemSecurity: The security the item should have
/// - mockValue: The mock value to set
public func setSaveMock<T: KeychainStorable>(item: T, itemSecurity: ItemSecurity = .standard, mockValue: Any) throws {
let query = try makeSaveQuery(item, itemSecurity: itemSecurity)
try ephemeralStrategy.setMock(mockValue, forQuery: query)
}

/// Retrieves the value for a call to `Haversack.save(_:itemSecurity:updateExisting:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameters:
/// - item: The item being saved
/// - itemSecurity: The security the item should have
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getSaveMock<T: KeychainStorable>(item: T, itemSecurity: ItemSecurity = .standard) throws -> Any? {
let query = try makeSaveQuery(item, itemSecurity: itemSecurity)
return try ephemeralStrategy.getMockDataValue(for: query)
}

/// Mocks data for calls to `Haversack.delete(_:treatNotFoundAsSuccess:)`
/// - Parameters:
/// - item: The item to generate a delete query and set a mock value for
/// - mockValue: The mock value to set
public func setDeleteMock<T: KeychainStorable>(item: T, mockValue: Any) throws {
let query = try makeDeleteQuery(item)
try ephemeralStrategy.setMock(mockValue, forQuery: query)
}

/// Retrieves the value for a call to `Haversack.delete(_:treatNotFoundAsSuccess:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter item: The item to generate a delete query and set a mock value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getDeleteMock<T: KeychainStorable>(item: T) throws -> Any? {
let query = try makeDeleteQuery(item)
return try ephemeralStrategy.getMockDataValue(for: query)
}

/// Mocks data for calls to `Haversack.delete(where:treatNotFoundAsSuccess:)`
/// - Parameters:
/// - query: The query to set a mock value for
/// - mockValue: The mock value to set
public func setDeleteMock<T: KeychainQuerying>(where query: T, mockValue: Any) throws {
let query = try makeDeleteQuery(query)
try ephemeralStrategy.setMock(mockValue, forQuery: query.query)
}

/// Retrieves the value for a call to `Haversack.delete(where:treatNotFoundAsSuccess:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameter query: The query to retrieve a value for
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getDeleteMock<T: KeychainQuerying>(where query: T) throws -> Any? {
let query = try makeDeleteQuery(query)
return try ephemeralStrategy.getMockDataValue(for: query.query)
}

/// Mocks data for calls to `Haversack.generateKey(fromConfig:itemSecurity:)`
/// - Parameters:
/// - config: The key generation configuration values that the query should include
/// - itemSecurity: The item security the query should specify
/// - mockValue: The mock value to set
public func setGenerateKeyMock(config: KeyGenerationConfig, itemSecurity: ItemSecurity = .standard, mockValue: SecKey) throws {
let query = try makeKeyGenerationQuery(fromConfig: config, itemSecurity: itemSecurity)
try ephemeralStrategy.setMock(mockValue, forQuery: query)
}

/// Retrieves the value for a call to `Haversack.generateKey(fromConfig:itemSecurity:)` from ``HaversackEphemeralStrategy/mockData``
/// - Parameters:
/// - config: The key generation configuration values that the query should include
/// - itemSecurity: The item security the query should specify
/// - Returns: The value associated with `query` in ``HaversackEphemeralStrategy/mockData``
public func getGenerateKeyMock(config: KeyGenerationConfig, itemSecurity: ItemSecurity = .standard) throws -> SecKey? {
let query = try makeKeyGenerationQuery(fromConfig: config, itemSecurity: itemSecurity)
return try ephemeralStrategy.getMockDataValue(for: query)
}
}

extension HaversackEphemeralStrategy {
/// Generates a ``mockData`` key for the query and sets the value of that key to `mockValue`
/// - Parameters:
/// - mockValue: The value to mock
/// - query: The query that the mock value is associated with
func setMock(_ mockValue: Any, forQuery query: SecurityFrameworkQuery) {
mockData[key(for: query)] = mockValue
}

/// Retrieves the ``mockData`` value for the provided query
///
/// This overload is required because `Optional<Any>` can be typecast to `Any`.
/// This means that if the generic version of this function were called where `T == Any`,
/// it would always return a non-nil value of `Any` with the actual type of `Optional<Any>.none`.
/// - Parameter query: The query to retreive a value for
/// - Returns: The value
func getMockDataValue(for query: SecurityFrameworkQuery) -> Any? {
mockData[key(for: query)]
}

/// Retrieves and typecasts the ``mockData`` value for the provided query
/// - Parameter query: The query to retreive a value for
/// - Returns: The typecasted value
func getMockDataValue<T>(for query: SecurityFrameworkQuery) -> T? {
getMockDataValue(for: query) as? T
}
}
9 changes: 7 additions & 2 deletions Sources/HaversackMock/HaversackEphemeralStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import Foundation
import Haversack

/// A strategy which uses a simple dictionary to search, store, and delete data instead of hitting an actual keychain.
/// A strategy which uses a simple dictionary to import, export, search, store, and delete data instead of hitting an actual keychain.
///
/// The keys of the ``mockData`` dictionary are calculated from the queries that are sent through Haversack.
open class HaversackEphemeralStrategy: HaversackStrategy {
Expand Down Expand Up @@ -95,6 +95,7 @@ open class HaversackEphemeralStrategy: HaversackStrategy {
/// - Parameter query: An instance of a `Haversack/SecurityFrameworkQuery`.
/// - Returns: Returns the private key of a new cryptographic key pair.
/// - Throws: An `NSError` object if the key cannot be found. Prior to throwing, also stores the query in the ``mockData`` for future inspection.
/// - Important: The mock value must be an instance of `SecKey`
override open func generateKey(_ query: SecurityFrameworkQuery) throws -> SecKey {
let theKey = key(for: query)

Expand Down Expand Up @@ -137,7 +138,11 @@ open class HaversackEphemeralStrategy: HaversackStrategy {
/// - Returns: The items in ``mockImportedEntities``
/// - Throws: Either ``mockImportError`` or an `NSError` with the ``errorDomain`` domain if the
/// items in ``mockImportedEntities`` don't match the type of the `EntityType` of the `configuration` parameter.
override open func importItems<EntityType: KeychainImportable>(_ items: Data, configuration: KeychainImportConfig<EntityType>, importKeychain: SecKeychain? = nil) throws -> [EntityType] {
override open func importItems<EntityType: KeychainImportable>(
_ items: Data,
configuration: KeychainImportConfig<EntityType>,
importKeychain: SecKeychain? = nil
) throws -> [EntityType] {
if let keyImportConfig = configuration as? KeychainImportConfig<KeyEntity> {
keyImportConfiguration = keyImportConfig
} else if let certificateImportConfig = configuration as? KeychainImportConfig<CertificateEntity> {
Expand Down
Loading